Skip to main content

How Gap Locks Block an INSERT in MySQL

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

An INSERT adds a brand-new row. So what could it possibly be waiting for? Under MySQL's default isolation level, it can be waiting for a gap: the space between rows, locked by a transaction that asked about a row that isn't there. Here's a gap lock, the insert it blocks, and how READ COMMITTED removes it.

Prerequisites​

  • MySQL, and two sessions, plus a root session to read the locks. The examples use MySQL 9.7.4 in Docker.
  • SELECT ... FOR UPDATE.

Step 1: Lock a Row That Doesn't Exist​

No product has the ID 1000, which is higher than any in the table:

mysql: terminal 1
START TRANSACTION;
SELECT Id, Sku FROM products WHERE Id = 1000 FOR UPDATE;
Output (the SELECT)
Empty set (0.001 sec)

No rows, yet there's a lock:

mysql: root session
SELECT INDEX_NAME, LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM performance_schema.data_locks
WHERE LOCK_TYPE = 'RECORD';
Output
+------------+-----------+-------------+------------------------+
| INDEX_NAME | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+------------+-----------+-------------+------------------------+
| PRIMARY | X | GRANTED | supremum pseudo-record |
+------------+-----------+-------------+------------------------+
1 row in set (0.001 sec)

supremum pseudo-record is an imaginary row after the last real one, and a lock on it covers the gap between them. MySQL's InnoDB locking page says: "A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record."

Step 2: Try to Insert into the Gap​

A new product gets the next AUTO_INCREMENT ID, which lands in that gap:

mysql: terminal 2
SET SESSION innodb_lock_wait_timeout = 4;
INSERT INTO products (Sku, Name, CategoryId, UnitPrice, QuantityOnHand, ReorderLevel, IsArchived, Version, CreatedAtUtc, UpdatedAtUtc)
VALUES ('GAP-TEST', 'Gap test', 1, 1, 0, 0, 0, 0, NOW(6), NOW(6));
Output (after 4 seconds)
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

An INSERT into products failing with ERROR 1205 Lock wait timeout exceeded, while another session's lock on the supremum pseudo-record holds the gap at the end of the table.

It waited for a lock on a row that doesn't exist, and gave up. Other tables aren't affected: an INSERT into categories at the same moment went straight through.

The page gives the reason: "Gap locks in InnoDB are 'purely inhibitive', which means that their only purpose is to prevent other transactions from inserting to the gap." That's how REPEATABLE READ makes a repeated FOR UPDATE return the same rows.

Step 3: READ COMMITTED Has No Such Lock​

mysql: terminal 1
ROLLBACK;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT Id, Sku FROM products WHERE Id = 1000 FOR UPDATE;
Output (the locks, from the root session)
Empty set (0.003 sec)

No locks at all, and the same INSERT goes through while the transaction is open:

Output (terminal 2)
Query OK, 1 row affected (0.004 sec)

The Deadlock This Causes​

Gap locks don't conflict with each other, and that sets a trap. Two sessions check for a row, find nothing, and both go to insert:

mysql: terminal 1
START TRANSACTION;
SELECT Id, Sku FROM products WHERE Id = 1000 FOR UPDATE;
mysql: terminal 2
START TRANSACTION;
SELECT Id, Sku FROM products WHERE Id = 1000 FOR UPDATE;

Each holds the gap, so each insert waits for the other:

Output (terminal 2's INSERT)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Fixed: Deadlock on INSERT After SELECT FOR UPDATE on a Missing Row covers the fix: insert first, and handle a duplicate.

Common Mistakes​

MistakeWhat happensDo this instead
"Check if it exists with FOR UPDATE, then insert"Gap locks, and deadlocks between two sessionsInsert, and handle the duplicate error
Holding a FOR UPDATE on a range open for longInserts into the range waitKeep it short
Assuming an INSERT can't waitIt waits for gap locks and for unique checksLook at data_locks

Troubleshooting​

SymptomLikely causeFix
An INSERT hangs, with no conflicting rowA gap lockSee who's blocking
ERROR 1213 on an INSERTTwo gap locks, and two insertsThe fix
A lock on supremum pseudo-recordA gap lock at the end of the indexEnd the transaction that holds it

FAQ​

Should I switch to READ COMMITTED to avoid them?​

Only if you've thought through what it changes: READ COMMITTED vs REPEATABLE READ in MySQL. Often the better fix is a query that doesn't need one, such as locking an existing row by its primary key, which takes no gap lock.

Conclusion​

Under REPEATABLE READ, a locking read also locks the gaps it passes through, including where a missing row would be. That's what makes an INSERT wait on nothing you can see. Look at data_locks for supremum pseudo-record or X,GAP, and avoid the "check, then insert" pattern.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.