skip to content

A relational engine can resolve stuck lock waits either by periodically searching a wait-for graph for cycles or by aborting any transaction that has waited longer than a configured lock-wait timeout. Compare the two approaches, and explain what a deadlock detector considers when choosing which transaction to abort.

level: middleimportance: should knowfreq 45%

answer

  1. graph = precise + fast, costs CPU
  2. timeout = cheap + blunt, kills innocents
  3. cycle invisible across nodes -> timeout only
  4. victim = least work / fewest locks / priority
  5. detector report names the statements

basics

~20 s

Graph detection finds real cycles quickly and aborts only a true participant; timeouts are cheap but slow and kill innocent waiters that were merely blocked by a slow holder. Victims are picked to minimise wasted work — fewest changes, fewest locks, shortest run, lowest priority.

solid answer

~60 s

**Wait-for-graph detection** models each active transaction as a node and each block as an edge; a cycle is by definition a deadlock. It is precise — only genuine participants abort — and fast, because the deadlock is resolved as soon as the sweep runs rather than after a fixed wait. Its cost is bookkeeping plus periodic graph traversal, which grows with concurrency, so engines run it on a short interval or only for transactions that have waited past a small threshold. **Lock-wait timeouts** need no graph at all: wait too long, get aborted. But a timeout cannot tell a cycle from a slow lock holder, so it aborts innocent waiters, and every real deadlock costs the full timeout in latency. Set it low and you kill legitimate waits; set it high and deadlocks hang. Most engines use detection for local deadlocks and keep a timeout as a backstop for waits the graph cannot see (external resources, distributed cases). **Victim selection** minimises wasted work and lost progress: rows modified or log volume, locks held, age, and any explicit priority. Applications cannot rely on being spared.

code

text · 6 lines
text
detector:  T1 -> T2 -> T3 -> T1      cycle found in ~ms, abort cheapest member
timeout:   T2 waited 50s             abort T2 -- was it a cycle? unknown

T4 blocked behind a slow but healthy holder:
  detector: no cycle -> T4 keeps waiting (correct)
  timeout:  T4 aborted anyway (false positive)

go deeper

for a junior

Know that the database can either find the cycle directly or simply give up after a wait limit, and that finding the cycle is the more precise option.

for a middle

Contrast precision and latency against implementation cost, explain why a timeout aborts innocent waiters, and list the victim-selection heuristics.

for a senior

Add the operational angle: read the engine's deadlock report to find conflicting access orders, keep the timeout as a backstop rather than the mechanism, and watch for starvation of small transactions.

for a principal

Reason about it across the whole system — where the wait-for graph structurally cannot reach (cross-node, external resources), what timeout values imply for connection-pool saturation and tail latency, and what deadlock-rate signals should trigger redesign.

## Two ways out of a stuck wait When a transaction blocks on a lock, the engine has to decide how long that is acceptable and how to tell a temporary wait from a permanent one. There are two mechanisms in practice. ### Wait-for-graph detection The lock manager already knows, for every blocked transaction, which transaction holds the lock it wants. That is exactly the data needed for a **wait-for graph**: nodes are active transactions, and an edge T1 → T2 means T1 is blocked on a lock T2 holds. A cycle in this graph is a deadlock, definitionally — no waiter in the ring can be satisfied without another releasing first, and none will release before finishing. Engines rarely run the search on every single wait, because most waits resolve in microseconds and a graph traversal per wait would be pure overhead at high concurrency. The common design is to start the check only after a transaction has waited past a small threshold, or to sweep the graph on a short timer. Either way the algorithm is a cycle search over a graph whose size is bounded by the number of *blocked* transactions, not all transactions. The strengths are precision and latency. Only a member of an actual cycle is aborted, so transactions that are merely waiting behind a slow but healthy holder are left alone. And resolution happens within the detection interval — typically well under a second — rather than after a timeout tuned for the worst legitimate wait. The weaknesses are cost and reach. Maintaining and traversing the graph consumes CPU and requires synchronisation on lock-manager structures, which matters when thousands of sessions are blocked. More importantly, the graph only covers waits the engine knows about. A transaction blocked on something outside the lock manager — an application-level mutex, a remote call, a lock held in another database node — is invisible, and no local detector will ever see that cycle. ### Lock-wait timeouts The alternative is to give up on diagnosis: any transaction that waits longer than a configured interval is aborted. This is trivial to implement, costs nothing while nothing is blocked, and works uniformly for every kind of wait, including the ones the graph cannot see. The price is bluntness. A timeout cannot distinguish "you are in a cycle and will wait forever" from "the holder is doing a slow but perfectly normal update", so it aborts innocent waiters whenever a lock is held longer than the threshold. And when there *is* a real deadlock, the participants sit idle for the entire timeout before anything happens — with a 50-second timeout, a deadlock costs 50 seconds of latency and a held connection. Tuning is a genuine dilemma: low values abort healthy work under load spikes, high values make deadlocks catastrophic for latency. ### How real systems combine them Most production engines run a detector for local deadlocks *and* keep a lock-wait timeout as a backstop. The detector handles the common case precisely and quickly; the timeout catches waits the detector structurally cannot resolve — cross-node waits in a distributed setup, waits on resources outside the lock manager, and any pathological case where the detector is disabled or falls behind. In distributed systems that lack a global wait-for graph, timeouts are often the *only* available mechanism, which is why distributed deadlocks are typically resolved by timeout and retry. ## Choosing the victim Once a cycle is found, one member must be aborted. The goal is to break the cycle at the lowest total cost, so engines weigh some combination of: - **Work already done** — rows modified, undo or log records generated. Aborting the transaction with the least to undo is both cheaper to roll back and cheaper to redo. - **Locks held** — a transaction holding many locks may be closer to completion and unblocking more waiters. - **Age** — very old transactions are sometimes protected to avoid starvation, where the same transaction is repeatedly chosen and never finishes. - **Explicit priority** — some engines let an operator or session mark work as more or less expendable. Two consequences follow for application design. First, victim choice is **not** under application control and not predictable, so every transaction that takes locks needs the same deadlock-retry handling — you cannot assume the batch job always loses and the interactive request always wins. Second, a transaction that does very little work is a *likely* victim, so a small, frequently-run statement colliding with a large batch may be aborted repeatedly, which is a starvation pattern worth watching for in metrics. ## What to look at operationally When deadlocks appear, the engine's deadlock report is the primary artifact: it names the transactions in the cycle, the locks each held and wanted, and the statements involved. That is what identifies the conflicting access orders. Timeout-driven aborts, by contrast, tell you almost nothing about *why* — which is another reason to prefer a detector and keep the timeout as a safety net rather than the mechanism.

  • Why do engines usually keep a lock-wait timeout even when a deadlock detector is running?
    The detector only sees waits inside its own lock manager. Waits on resources it does not manage — a remote node in a distributed transaction, an advisory or application-level lock, an external service held open inside a transaction — form cycles the graph cannot represent. The timeout is the backstop that guarantees no session waits forever, and in distributed settings without a global wait-for graph it is often the only mechanism available.
  • Why might one particular transaction be chosen as the victim over and over?
    Victim heuristics favour aborting the transaction with the least work done, so a small, fast transaction repeatedly colliding with a large batch tends to lose every time — a starvation pattern. Some engines mitigate it by factoring in transaction age or letting operators set priorities. Operationally, the fix is usually to change the access ordering or split the batch, not to fight the heuristic.

saying these in an interview costs you the question

  • Claiming a lock-wait timeout is a deadlock detector — it cannot distinguish a cycle from a slow holder
  • Believing the longest-running transaction is always the victim
  • Assuming cycle detection runs on every lock wait rather than after a threshold or on a timer
  • Thinking a local detector can resolve deadlocks that span database nodes
  • Setting the lock-wait timeout very low as a 'deadlock fix', which just aborts healthy waiters

context