Symptoms
Applications log:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
This is different from a lock wait timeout (ERROR 1205), where a transaction waited longer than innodb_lock_wait_timeout. With a deadlock, InnoDB detects the cycle straight away and rolls back the transaction that's cheapest to undo (the "victim").
An occasional deadlock is normal on a busy OLTP system. A steady rate, or a sudden increase after a release, needs fixing.
1. See the latest deadlock
SHOW ENGINE INNODB STATUS\G
Find the LATEST DETECTED DEADLOCK section:
*** (1) TRANSACTION:
TRANSACTION 4211, ACTIVE 0 sec starting index read
UPDATE accounts SET balance = balance - 10 WHERE id = 7
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `bank`.`accounts` ... lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS ... index PRIMARY of table `bank`.`accounts` ... lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
...
UPDATE accounts SET balance = balance + 10 WHERE id = 3
*** WE ROLL BACK TRANSACTION (1)
Note these points:
- The statements each transaction was running. Earlier statements in the same transaction aren't shown, so check the app code for those.
- The index where the locks were taken:
PRIMARYor a secondary index. - The lock mode:
X(exclusive),S(shared),gap/insert intention(range locking).
2. Log every deadlock
Only the latest deadlock is kept in the status output. To log all of them to the error log:
SET PERSIST innodb_print_all_deadlocks = ON;
On RDS/Aurora and Azure, set it in the parameter group or server parameters, then read the error log from the console or CLI.
Count how often they happen:
SELECT NAME, COUNT FROM information_schema.INNODB_METRICS WHERE NAME = 'lock_deadlocks';
(Enable the counter first if it shows as disabled: SET GLOBAL innodb_monitor_enable = 'lock_deadlocks';.)
3. Current lock waits (while it's happening)
SELECT r.trx_id AS waiting_trx, r.trx_query AS waiting_query,
b.trx_id AS blocking_trx, b.trx_query AS blocking_query,
b.trx_started, TIMESTAMPDIFF(SECOND, b.trx_started, NOW()) AS blocking_age_s
FROM performance_schema.data_lock_waits w
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID;
The sys.innodb_lock_waits view gives the same information in a friendlier format.
4. Common patterns and fixes
| Pattern | Example | Fix |
|---|---|---|
| Opposite update order | T1 updates row A then B, T2 updates B then A | Always lock rows in the same order (for example by primary key) |
| Missing index on the WHERE column | UPDATE orders SET ... WHERE customer_ref = ? scans and locks many rows | Index the filter column so only the target rows are locked |
| Gap locks under REPEATABLE READ | Concurrent inserts into the same range after a SELECT ... FOR UPDATE | Use exact-match lookups on unique keys, or READ COMMITTED where the app allows it |
| Long transactions | User think time or remote calls inside a transaction | Keep transactions short; do the remote calls before or after |
| Foreign keys | Inserting child rows takes shared locks on parent rows | Consistent ordering; avoid updating parent rows in the same hot path |
| Batch jobs vs OLTP | A large UPDATE ... WHERE status = 'X' runs alongside app traffic | Break the batch into small chunks by primary key and commit each one |
Check the plan of the statements involved:
EXPLAIN UPDATE orders SET status = 'shipped' WHERE customer_ref = 'C-1042';
type: ALL or a large rows estimate means the statement locks far more rows than it changes.
Isolation level
MySQL's default is REPEATABLE READ, which uses gap and next-key locks for range scans. Many applications work correctly with READ COMMITTED, which takes far fewer gap locks:
SET PERSIST transaction_isolation = 'READ-COMMITTED';
Warning
Changing the isolation level changes application behaviour (repeatable reads within a transaction). Test it, and keep binary logging in ROW format, which READ COMMITTED requires.
5. Retries in the application
Deadlocks can't be eliminated completely. The application must retry the whole transaction on error 1213:
import time, random, pymysql
def run_with_retry(conn, work, attempts=3):
for i in range(attempts):
try:
with conn.cursor() as cur:
work(cur)
conn.commit()
return
except pymysql.err.OperationalError as e:
conn.rollback()
if e.args[0] != 1213 or i == attempts - 1:
raise
time.sleep(0.05 * (2 ** i) + random.random() * 0.05)
Prevent it next time
- Keep
innodb_print_all_deadlockson and alert on the rate. - Review new queries for index use on UPDATE/DELETE predicates.
- Consistent lock ordering as a coding rule for multi-row transactions.
- Chunk batch jobs and run them outside peak hours.
Checklist
- Deadlock details captured (statements, index, lock modes)
- All deadlocks logged; rate measured
- Missing indexes on locking statements fixed
- Consistent lock order in code
- Transactions shortened, batches chunked
- Application retries on 1213