Control panels

Troubleshooting MySQL deadlocks and transaction failures

Diagnose MySQL deadlocks and transaction failures using the InnoDB deadlock report, live lock queries and safer transaction ordering.

AdvancedLinux server6 min read

MySQL deadlock troubleshooting starts with reading the report InnoDB writes when it kills one of your transactions. A deadlock is not a MySQL bug: two transactions each hold a row lock the other one wants, InnoDB detects the cycle and rolls one back with error 1213. This guide walks through that report, the live lock tables, and the transaction changes that stop the same pair of queries colliding again.

Before you start

  • A Linux VPS running MySQL 8.0/8.4 or MariaDB 10.6 or newer, with root or sudo access.
  • SSH access as a sudo-capable user such as deploy. See Connect to your Linux VPS with SSH if you have not set that up.
  • A MySQL account with PROCESS and read access to performance_schema. Root works for a one-off investigation.
  • The application error log or the client that reported the failure, so you know which query failed.
  • A staging copy of the database if you plan to change indexes or isolation levels.

Log in and open a session:

Bash
ssh deploy@203.0.113.10
mysql -u root -p

MySQL deadlock troubleshooting: read the InnoDB report

Two errors matter here:

  • ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction — InnoDB picked your transaction as the victim and rolled it back.
  • ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction — no cycle was detected, but a lock was held longer than innodb_lock_wait_timeout.

Show the most recent deadlock:

SQL
SHOW ENGINE INNODB STATUS\G

Scroll to LATEST DETECTED DEADLOCK. You get the timestamp, both transactions, the statement each one was running, the rows and index it locked, and which transaction was rolled back. That block is the whole diagnosis, so read it before changing anything.

To keep every deadlock instead of only the last one:

SQL
SET GLOBAL innodb_print_all_deadlocks = ON;

Warning: this writes every deadlock to the error log. On a busy server the log can grow quickly, so enable it while you investigate and turn it back off afterwards.

The log is usually /var/log/mysql/error.log on Debian and Ubuntu, and /var/log/mysqld.log on AlmaLinux and Rocky. Check Where is my.cnf? if your paths differ.

Inspect the live locks and transactions

When the problem is happening right now, look at what holds what:

SQL
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
       trx_query, trx_rows_locked, trx_rows_modified
FROM information_schema.INNODB_TRX\G
SQL
SELECT ENGINE_TRANSACTION_ID, OBJECT_NAME, INDEX_NAME,
       LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM performance_schema.data_locks\G
SQL
SELECT * FROM performance_schema.data_lock_waits\G

data_lock_waits is the fastest view: it names the blocking transaction and the waiting one. On MariaDB the same information lives in information_schema.INNODB_LOCKS and INNODB_LOCK_WAITS, so adjust the query if you are not on MySQL 8.

To see the SQL behind a running thread:

SQL
SELECT THREAD_ID, SQL_TEXT, TIMER_WAIT/1000000000 AS ms
FROM performance_schema.events_statements_current;

Warning: killing a transaction with KILL <thread_id> rolls back everything it did. Only do it on a transaction you have positively identified, and never kill the wrong thread on a production server.

Find the missing index or the wide lock

Most deadlocks come from one of three patterns:

  1. Two transactions update the same rows in a different order (A then B versus B then A).
  2. A statement scans without a usable index, so InnoDB locks far more rows than the query returns.
  3. A transaction stays open for seconds while doing work outside the database.

Check the plan for the statement from the deadlock report:

SQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 FOR UPDATE;

If type is ALL or key is NULL, add the index the query needs and the lock footprint shrinks immediately. Under REPEATABLE READ, InnoDB also takes gap locks on ranges, which is why an unindexed SELECT... FOR UPDATE can deadlock with a plain INSERT.

Tip: run ANALYZE TABLE after adding an index so the optimiser picks it instead of falling back to a full scan.

Change the transaction so it stops deadlocking

  • Touch tables and rows in the same order in every code path. Sort IDs before updating a set.
  • Keep transactions short: no HTTP calls, file writes or sleeps between BEGIN and COMMIT.
  • Add retry logic for errors 1213 and 1205. Roll back, wait a short random delay, then run the transaction again. Most deadlocks disappear on the second attempt.
  • Use SELECT... FOR UPDATE only on rows you will actually change, and prefer primary-key lookups.
  • Consider READ COMMITTED for high-contention OLTP workloads, which reduces gap locking.
SQL
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

Warning: changing the isolation level changes visibility rules for the whole session. Test on staging first, and do not set it globally on a production server without checking what your application expects.

Check it works

Run the workload that used to fail, then confirm the counter has stopped moving:

SQL
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';

Check the error log for new entries:

Bash
sudo tail -n 100 /var/log/mysql/error.log | grep -i deadlock

Also confirm your retry wrapper logs the second attempt as a success rather than swallowing the error. Once the counter is flat under normal load, turn innodb_print_all_deadlocks back off.

Troubleshooting

  • Deadlocks keep appearing with no obvious cycle — a long-running transaction is holding locks. Look at trx_started in INNODB_TRX; anything running for minutes is the cause.
  • Lock wait timeout exceeded but no deadlock — raise innodb_lock_wait_timeout only after shortening transactions, otherwise you just make users wait longer.
  • Deadlock only appears under load — retry logic is missing or too fast. Add jitter and a small backoff.
  • The same statement deadlocks after adding an index — check the index column order matches the WHERE and ORDER BY clauses, and refresh statistics with ANALYZE TABLE.
  • The application reports 1213 but the server log is empty — innodb_print_all_deadlocks is off, or you are reading the wrong error log.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.