Debugging Transaction Contention in CockroachDB
Transaction contention is usually invisible until it isn’t — throughput looks fine, then a handful of hot rows turn into a queue of blocked transactions and your p99 latency falls off a cliff. CockroachDB ships good tooling for this, but it’s easy to miss if you don’t know where to look.
Start with the contention views
CockroachDB exposes contention data directly in SQL:
| |
This gives you the blocking and waiting transaction fingerprints, the table
and index involved, and how long the wait lasted. For a higher-level view,
crdb_internal.cluster_contended_tables aggregates by table so you can
quickly spot the hot spot:
| |
The usual suspects
In practice, most contention I’ve debugged falls into one of three patterns:
- A monotonically increasing key. Sequences,
now()-derived primary keys, or auto-incrementing IDs all funnel writes onto the same range and the same leaseholder.INSERT INTO events (id, ...) VALUES (gen_random_uuid(), ...)spreads writes far better than a sequential integer. - A single “counter” or “status” row. Things like an account balance or a job queue’s status column get updated by every transaction touching that entity, serializing work that didn’t need to be serialized.
- Long-running transactions holding locks. A transaction that does an
API call or heavy computation between its first write and its commit
holds locks the whole time. Keep transactions short and push non-database
work outside the
BEGIN/COMMITboundary.
A concrete fix: splitting a hot counter
If you have a table like this:
| |
and many concurrent workers decrementing quantity for the same popular
SKU, consider sharding the counter:
| |
This trades a small amount of read complexity for a large reduction in write contention, since decrements are now spread across eight rows instead of one.
Takeaway
Contention debugging in CockroachDB is mostly about asking “what’s the
minimum set of rows this transaction actually needs to touch, and for how
long?” The built-in crdb_internal views make it easy to confirm a
hypothesis — the harder part is usually admitting that a hot row in your
schema was inevitable given the access pattern, and redesigning around it.