Skip to main content
MySQL · lesson 5 of 8
MySQL intermediate Lesson 5 of 8

A Real Deadlock: Error 1213, and the Lock-Order Fix

Two sessions locking the same two rows in opposite order produce a genuine ERROR 1213 deadlock, not a hang — MySQL detects it and kills one transaction immediately. Locking in a consistent order eliminates it entirely, replaced by an ordinary wait.

By SkillByExample · Published

mysql locking deadlocks for-update innodb

Two sessions, two rows, opposite locking order — and MySQL’s real response isn’t a hang, it’s an immediate, specific error.

A lock that makes a second session wait

-- Session A
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT SLEEP(20);
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- Session B, started a few seconds into session A's sleep
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- session A's real timestamps
session_a_start   2026-09-13 17:31:05
session_a_end     2026-09-13 17:31:25

-- session B's real timestamps
session_b_start   2026-09-13 17:31:14
session_b_end     2026-09-13 17:31:25

Session B’s UPDATE started at 17:31:14 — while session A’s FOR UPDATE lock on row 1 was still held — and didn’t finish until 17:31:25, the exact same instant session A committed. Session B wasn’t stuck or erroring; it was correctly waiting for a lock that session A was holding, and proceeded the moment that lock released.

-- checked while both sessions were live
SELECT * FROM performance_schema.data_lock_waits;
REQUESTING_ENGINE_TRANSACTION_ID  BLOCKING_ENGINE_TRANSACTION_ID
1851                               1850

Direct confirmation from the server itself: transaction 1851 (session B) is waiting on a lock held by transaction 1850 (session A) — not inference from timing, the actual recorded lock-wait relationship.

The bug: two sessions, opposite lock order, a real deadlock

-- Session A: locks row 1, then (after a delay) wants row 2
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT SLEEP(12);
SELECT * FROM accounts WHERE id = 2 FOR UPDATE;
COMMIT;
-- Session B: locks row 2, then (after a longer delay) wants row 1
START TRANSACTION;
SELECT * FROM accounts WHERE id = 2 FOR UPDATE;
SELECT SLEEP(20);
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
COMMIT;
-- session A
a_locks_1   2026-09-13 17:42:44.712073
a_tries_2   2026-09-13 17:42:56.713882
a_got_2     2026-09-13 17:43:13.687709

-- session B
b_locks_2   2026-09-13 17:42:53.658025
b_tries_1   2026-09-13 17:43:13.659352
ERROR 1213 (40001) at line 7: Deadlock found when trying to get lock; try restarting transaction

Both sessions ended up waiting on each other: A held row 1 and wanted row 2 (which B held); B held row 2 and wanted row 1 (which A held). Neither could ever proceed by waiting longer — a genuine deadlock. MySQL didn’t hang: the instant B’s request for row 1 completed the cycle (17:43:13.659), InnoDB detected it and immediately killed session B’s transaction with a real, specific error. Session A’s blocked request for row 2 then succeeded 28 milliseconds later (17:43:13.687), the moment B’s rollback released its lock.

The fix: lock in a consistent order

-- Both sessions now lock row 1 before row 2, no exceptions
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- ... then row 2 ...
SELECT * FROM accounts WHERE id = 2 FOR UPDATE;
COMMIT;
-- session A (holds row 1 first, sleeps 10s, then locks row 2)
a_locks_1   2026-09-13 17:50:14.895280
a_tries_2   2026-09-13 17:50:24.897809
a_got_2     2026-09-13 17:50:24.898238

-- session B (also tries row 1 first, arriving mid-way through A's hold)
b_tries_1_first  2026-09-13 17:50:24.030004
b_got_1          2026-09-13 17:50:24.898945
b_got_2          2026-09-13 17:50:24.899975

No error this time. Session B requested row 1 at 17:50:24.030 and simply waited — the same kind of ordinary block from the first example — until session A committed at 17:50:24.898, then acquired both locks in the same order (1, then 2) without contention. With every transaction agreeing on lock order, the cycle that caused the earlier deadlock can never form: there’s no scenario where one session holds row 2 while waiting on row 1, because nothing ever locks row 2 first.

Practice

1. Check whether any lock waits are currently active, without knowing in advance which transactions are involved.
SELECT * FROM performance_schema.data_lock_waits;
Empty set (0.00 sec)

Empty when nothing is currently blocked — this is the query to run in production when a report of “the app is hanging” needs a real answer instead of a guess. information_schema .innodb_lock_waits, documented in older MySQL guides, was actually removed in MySQL 8.4 — performance_schema.data_lock_waits is the current table, confirmed here by the same query that caught the real lock wait earlier in this lesson.

2. Confirm the deadlock victim's changes were fully rolled back, not partially applied.
SELECT balance FROM accounts WHERE id = 1;
balance
500.00

Session B (the deadlock victim) never got to run its own update to row 1 — its entire transaction was rolled back atomically when InnoDB killed it, so accounts.id = 1 shows only session A’s completed change, with no partial or corrupted state left behind by B’s aborted attempt.

3. Confirm the winning transaction's work was fully committed despite the deadlock happening around it.
SELECT * FROM accounts WHERE id IN (1, 2);
id  name   balance
1   Alice  500.00
2   Bob    300.00

Session A’s transaction committed successfully once it acquired both locks — a deadlock only rolls back the chosen victim, never the transaction that “wins” the cycle. From A’s perspective, the deadlock was invisible except for the extra wait before its second lock request succeeded.

Next: stored procedures and triggers — behavior that fires automatically, and what a syntax tutorial doesn’t show about it.

Frequently Asked Questions

Why did the second session's UPDATE wait instead of failing immediately?
SELECT ... FOR UPDATE takes an exclusive row lock that's held until the transaction commits or rolls back — a second session trying to write (or also FOR UPDATE-lock) the same row has to wait for that lock to release. This is normal, expected blocking, not an error condition; it only becomes a deadlock when the wait can never resolve on its own.
What actually makes a deadlock different from one session just waiting a long time?
A deadlock is a genuine cycle: session A holds a lock session B wants, while session B holds a lock session A wants, so neither can ever proceed no matter how long they wait. A single session blocking on a lock eventually gets it once the holder commits — a deadlock cycle never resolves by waiting, since both sides are simultaneously the blocker and the blocked.
How did MySQL know a deadlock existed, and how quickly?
InnoDB actively tracks the lock-wait graph and detects cycles as they form, rather than waiting for a timeout — as soon as session B's request for row 1 completed the cycle (A wants row 2, B wants row 1, each blocking the other), InnoDB immediately rolled back one of the two transactions and returned Error 1213 to it, near-instantly rather than after any wait.
How does MySQL decide which of the two transactions to kill?
InnoDB generally chooses the transaction that has done less work (fewer modified rows) as the deadlock victim, on the theory that rolling it back is cheaper — but this isn't a guarantee application code should rely on. Any transaction can be the victim, which is exactly why real code needs to catch Error 1213 and be prepared to retry, rather than assuming a particular session will always win.
Does locking rows in a consistent order really prevent every deadlock?
It prevents this specific, very common category — a cycle formed by two transactions acquiring the same set of locks in opposite orders. If every transaction that touches rows 1 and 2 always locks 1 before 2, no cycle can ever form between them, because there's no scenario where one transaction holds 2 while waiting for 1. It doesn't prevent every conceivable deadlock (more complex lock types and larger transaction graphs can still deadlock), but it eliminates the most frequent real-world cause.