Blocking sessions are the most common cause of “the database is slow but everything looks healthy.” One session holds an uncommitted transaction on a row; every other session that wants to touch that row queues behind it on enq: TX - row lock contention. The instance is OPEN, the listener responds, CPU is often low, and TPS quietly decays. From the outside it looks like a performance regression rather than a lock incident.
The trap during diagnosis is that V$SESSION.BLOCKING_SESSION only points at a session’s immediate blocker, not the head of the chain. When you have three or four levels of dependency, killing the session that most waiters point at can either do nothing (the chain re-points one level up) or roll back a transaction that was doing legitimate work. The skill is finding the single root blocker, confirming it is genuinely idle, and killing only that one.
This guide covers the chain model, the views that resolve it correctly, the page thresholds, and the safe kill procedure. It assumes you already understand Oracle’s wait event model and enqueue types. See How Oracle Database actually works in production for that background.
What this means
Oracle enqueues serialize access to logical resources. The two you will see most in a blocking incident are enq: TX - row lock contention (a session has uncommitted DML on a row and another wants to modify the same row) and enq: TM - contention (almost always an unindexed foreign key locking the child table during a parent delete or update). Less common but worth knowing: enq: TX - allocate ITL entry means the block header needs more INITRANS, and enq: TX - index contention is transient leaf block splitting.
The chain forms like this: the head session holds a row lock and has not committed. A second session blocks on it, waiting for that row. If that second session has its own uncommitted transaction holding a different row, a third session can be blocked by the second. V$SESSION.BLOCKING_SESSION for the third session shows the second, not the head. Naive queries that count which SID appears most often as a BLOCKING_SESSION value will finger the intermediate session, and killing it rolls back a transaction whose locks were not the root cause.
flowchart TD H["Head blocker INACTIVE, uncommitted TX"] B["Session B blocked by H, holds own TX"] C1["Waiter C"] C2["Waiter D"] C3["Waiter E"] H -->|"holds TX lock"| B B -->|"enqueue wait"| C1 B -->|"enqueue wait"| C2 B -->|"enqueue wait"| C3
C, D, and E all show BLOCKING_SESSION pointing at B in V$SESSION. The head of the chain is H. Killing B accomplishes nothing useful; the waiters re-block on whatever B was waiting for, and you have rolled back B’s transaction for no gain.
A second trap: the head blocker almost always looks INACTIVE in V$SESSION. It is sitting on SQL*Net message from client, burning no CPU and doing no I/O. Standard “top sessions by CPU” dashboards will not surface it. You have to look specifically for enqueue waiters and walk the chain.
Page thresholds: any single blocker with more than 10 waiters, or any session blocked for more than 5 minutes with a growing chain, is a PAGE. The distinguishing feature versus the archive hang (the other “looks up but is frozen” pattern) is the wait event: a lock cascade shows enq: TX on a subset of sessions while other sessions still succeed, while an archive hang shows log file switch (archiving needed) across all sessions uniformly.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Application not committing | Blocker is INACTIVE, one SQL_ID, enq: TX waits growing, TPS dropping | V$SESSION for the blocker: STATUS, SQL_ID, SECONDS_IN_WAIT |
| Developer tool left a transaction open | One idle session, small undo footprint, many waiters | V$SESSION.USERNAME and PROGRAM for the blocker |
| Unindexed foreign key (TM contention) | enq: TM - contention, child table locked on parent DML | DBA_INDEXES for FK columns on objects in V$LOCKED_OBJECT |
| Batch job holding locks long | One ACTIVE session, large USED_UBLK in V$TRANSACTION, OLTP blocked | V$TRANSACTION.USED_UBLK and V$SESSION_LONGOPS for the batch |
| Insufficient ITL slots | enq: TX - allocate ITL entry, many sessions on one hot block | Segment INITRANS |
Quick checks
All of these are read-only and safe to run during an incident.
-- Current enqueue waiters, longest first
SELECT s.SID, s.SERIAL#, s.USERNAME, s.EVENT, s.SECONDS_IN_WAIT,
s.SQL_ID, s.BLOCKING_SESSION, s.BLOCKING_SESSION_STATUS
FROM V$SESSION s
WHERE s.EVENT LIKE 'enq:%' AND s.STATE = 'WAITING'
ORDER BY s.SECONDS_IN_WAIT DESC;
-- Which sessions are blocking others right now (naive: shows immediate blockers, not heads)
SELECT BLOCKING_SESSION, COUNT(*) AS waiters
FROM V$SESSION
WHERE BLOCKING_SESSION_STATUS = 'VALID'
GROUP BY BLOCKING_SESSION
ORDER BY waiters DESC;
-- Blocker detail: is the head INACTIVE or ACTIVE?
SELECT SID, SERIAL#, USERNAME, STATUS, PROGRAM, SQL_ID,
SECONDS_IN_WAIT, EVENT, BLOCKING_SESSION, BLOCKING_SESSION_STATUS
FROM V$SESSION
WHERE SID IN (
SELECT BLOCKING_SESSION FROM V$SESSION
WHERE BLOCKING_SESSION_STATUS = 'VALID'
);
-- Lock detail: who holds (BLOCK=1) vs who requests (REQUEST>0)
SELECT l.SID, l.TYPE, l.LMODE, l.REQUEST, l.BLOCK, l.CTIME,
o.OWNER, o.OBJECT_NAME
FROM V$LOCK l
LEFT JOIN V$LOCKED_OBJECT lo ON l.SID = lo.SESSION_ID
LEFT JOIN DBA_OBJECTS o ON lo.OBJECT_ID = o.OBJECT_ID
WHERE l.BLOCK = 1 OR l.REQUEST > 0
ORDER BY l.BLOCK DESC, l.CTIME DESC;
In RAC, use GV$LOCK and GV$SESSION for cluster-wide visibility, otherwise you can miss a cross-instance blocker. The legacy DBA_BLOCKERS and DBA_WAITERS views are single-instance only and unreliable for RAC; prefer the GV$ views.
How to diagnose it
Confirm the symptom is locking, not an archive hang or redo stall. The wait event must be
enq: TXorenq: TMon a subset of sessions, with other sessions still succeeding.List the enqueue waiters ordered by
SECONDS_IN_WAIT. The longest-waiting sessions are the ones closest to user-visible impact.Resolve the chain to its head.
BLOCKING_SESSIONalone is not enough for chains deeper than one level. Two reliable options:- Walk the chain manually. For each waiter, look up its
BLOCKING_SESSION, then look up that session’sBLOCKING_SESSION, until you reach a session whoseBLOCKING_SESSIONis null or whoseBLOCKING_SESSION_STATUSis not VALID. That session is the head. - Use
FINAL_BLOCKING_SESSIONonV$SESSION, which Oracle resolves internally to the head of the chain whenFINAL_BLOCKING_SESSION_STATUS = 'VALID'. V$WAIT_CHAINSis the dedicated view for multi-level chains; a row whose blocker is not valid is the head.
- Walk the chain manually. For each waiter, look up its
Confirm the head is genuinely idle and not doing useful work. Check
STATUS(INACTIVE is the classic smoking gun),SQL_ID(what it last ran),PROGRAMandUSERNAME(a person in a SQL tool, a batch job, or a connection pool?), andSECONDS_IN_WAIT(how long onSQL*Net message from client).Capture everything before touching it. Record SID, SERIAL#, SQL_ID, USERNAME, PROGRAM, the waiter count, and
USED_UBLKfromV$TRANSACTIONif the head has an open transaction. You will need this if the kill turns out to be the wrong call or if someone asks for a postmortem.Correlate with undo. If the head is a large uncommitted batch transaction, killing it rolls back a lot of undo and generates redo. Check
V$TRANSACTION.USED_UBLKfor the head’s SADDR. A multi-gigabyte rollback is itself an incident.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
enq: TX - row lock contention wait time | Primary lock-contention signal | Greater than 5% of DB time sustained |
| Sessions with BLOCKING_SESSION set | Live blocking chains | Any chain growing over minutes |
| Waiters per blocker | Cascade severity | One SID blocking more than 10 sessions |
| Max SECONDS_IN_WAIT on enqueue waits | How long victims have been stuck | Any session above 300 seconds |
| V$LOCK TX with LMODE=6 and CTIME above 300 | Long-held exclusive locks by idle sessions | Any non-batch session |
| TPS (user commits) | Throughput decay from locking | More than 50% drop from baseline |
| ORA-00060 in alert log | Deadlocks, auto-resolved, separate issue | Any sustained rate |
Fixes
The only operational fix for an active lock cascade is to remove the head blocker. The right tool is almost always ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE. IMMEDIATE terminates the session without waiting for it to self-terminate; PMON still does the rollback.
Before you run it, verify you have the SID and SERIAL# of the head, not an intermediate session. If you are wrong, the chain re-points one level up and you have rolled back a transaction for nothing. If the head is a batch job mid-run, killing it may roll back hours of work. Talk to the owner first if there is time.
Kill commands are destructive. They roll back the target transaction, which generates redo and undo activity. Do not fire multiple parallel kills if the sessions are part of the same chain; let each one settle and re-check the chain. In RAC, append @inst_id to target a specific instance: ALTER SYSTEM KILL SESSION 'sid,serial#@inst_id'.
For the underlying causes:
- Unindexed foreign key. Add an index on the FK column. This permanently removes
enq: TM - contentionfor that parent/child pair. - Insufficient ITL. Rebuild the segment or index with higher
INITRANS(andMAXTRANSon older releases). This addressesenq: TX - allocate ITL entry. - Application not committing. This is an application fix, not a database fix. The kill is a bandage.
Oracle’s deadlock detector resolves true two-session cycles automatically by killing one statement with ORA-00060. If you are seeing massive enqueue waits but no ORA-00060 in the alert log, you do not have deadlocks. You have long-held locks, and the deadlock detector will not help you.
Prevention
- Index every foreign key unless you have a documented reason not to. This is the single highest-value prevention step for locking incidents.
- Monitor
V$LOCKfor TX exclusive locks withCTIMEabove 300. The playbook calls this out as a signal critical during incidents but rarely proactively monitored. Alert on it. - Push the application to use bind variables and explicit commits. Open transactions during user think time are the root cause of most cascades.
- On 19c and later, consider
MAX_IDLE_BLOCKER_TIME. It automatically terminates idle sessions that are holding blocking locks. Default is 0 (disabled). Set it belowMAX_IDLE_TIMEto be effective. It does not affect SYS sessions or parallel query slaves. - On 23ai, evaluate Priority Transactions and Lock-Free Reservations. Priority Transactions can auto-rollback a low-priority blocker holding a high-priority transaction; Lock-Free Reservations allow concurrent updates to hot numeric columns without blocking.
How Netdata helps
- Per-second collection of active sessions and the dominant wait class means a lock cascade shows up as a Concurrency or Application wait spike within seconds of forming, long before users open tickets.
- Correlating the enqueue wait spike against TPS, session count, and process utilization confirms whether the cascade is exhausting connection capacity, which is the escalation path from “slow” to “application down.”
- Trending enqueue deadlock counts and lock-wait time over days surfaces applications that habitually hold locks too long, so the fix happens before the next 3 a.m. page.
- Anomaly detection on the wait-class mix flags the moment the database shifts from CPU- or I/O-bound into Concurrency waits, which is the signature of a fresh blocking chain.
Netdata’s Oracle Database monitoring brings these signals together with per-second metrics and ML anomaly detection.
Related guides
- How Oracle Database actually works in production: a mental model for operators
- Oracle archive log destination full: V$ARCHIVE_DEST_STATUS, the ERROR state, and space
- Oracle autoextend hit MAXSIZE: the space gotcha with a half-empty filesystem
- Oracle ‘Thread N cannot allocate new log’: the archive hang that masquerades as up
- Oracle ‘Checkpoint not complete’: redo log sizing, DBWn, and log-switch stalls
- Oracle ’enq: TX - row lock contention’: blocking sessions and uncommitted DML
- Oracle Fast Recovery Area full: db_recovery_file_dest_size, reclaimable space, and DELETE OBSOLETE
- Oracle ’log file sync’ waits: slow commits, LGWR, and the redo path
- Oracle Database monitoring checklist: the signals every production instance needs
- Oracle Database monitoring maturity model: from survival to expert
- ORA-00257: archiver error, connect internal only until freed
- ORA-01555: snapshot too old, rollback segment too small






