CockroachDB transaction retry rate high: breaking down txn_restarts by cause
When application latency creeps upward without an obvious cause, or you start seeing PostgreSQL error code 40001 (serialization failure) in application logs, the transaction retry rate is likely climbing. CockroachDB exposes txn_restarts as a set of counters, each tagged by cause. The aggregate number tells you retries are happening. It does not tell you why.
The five dominant causes are writetooold, serializable, readwithinuncertainty, txnaborted, and txnpush. Three additional sub-metrics (asyncwritefailure, commitdeadlineexceeded, unknown) cover edge cases. A spike dominated by readwithinuncertainty is a clock infrastructure problem. A spike dominated by writetooold is a schema or application contention problem. The fix for one does nothing for the other.
For well-designed OLTP workloads, retry rates below 2% of total transactions are typical. The 2-10% range warrants investigation. Above 10%, you are burning significant resources on retry overhead and effective throughput is lower than it appears.
What this means
CockroachDB uses serializable isolation by default. Every transaction gets a timestamp from the node’s Hybrid Logical Clock. When two concurrent transactions touch overlapping keys, one may need to restart at a new timestamp. Some restarts happen transparently (automatic retry for implicit single-statement transactions); others surface as error code 40001 to the application.
Each restart cause maps to a different failure mode:
- writetooold: A transaction tried to write a key that another transaction already wrote at a higher timestamp. Most common under write contention. Typically indicates hot keys or sequential key patterns.
- readwithinuncertainty: A read on one node encountered a write from another node within the uncertainty window created by clock offset. Nearly diagnostic of clock skew. Any sustained nonzero rate means NTP needs attention.
- serializable: A write-write conflict where the writer’s timestamp was pushed, invalidating its prior reads. Encompasses high-priority transactions pushing lower-priority ones and long-running transactions hitting the closed timestamp interval.
- txnaborted: The transaction was aborted, typically due to priority-based deadlock resolution where two transactions hold intents on keys the other needs.
- txnpush: A transaction conflict where one transaction needs to push another’s timestamp forward to resolve a read-write or write-write conflict.
Break down the total before investigating.
flowchart TD
A["txn_restarts elevated"] --> B{"Dominant cause?"}
B -->|"writetooold"| C["Contention: schema or app design"]
B -->|"readwithinuncertainty"| D["Clock skew: NTP infrastructure"]
B -->|"serializable"| E["Write conflicts: contention events"]
B -->|"txnaborted"| F["Deadlocks: lock ordering"]
B -->|"txnpush"| G["App tx conflicts: patterns"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| writetooold | Dominates under write contention. Hot keys, sequential primary keys, single-row counters. | Hot Ranges page or crdb_internal.ranges ordered by QPS |
| readwithinuncertainty | Appears when clock offset between nodes is elevated. Often correlates with clock_offset_meannanos rising. | Clock offset metrics and NTP status on all nodes |
| serializable | Write-write conflicts, intent pushes by higher-priority transactions, long-running transactions exceeding closed timestamp. | crdb_internal.transaction_contention_events |
| txnaborted | Transactions acquiring locks in reverse order, causing deadlock resolution. | Application transaction patterns and lock ordering |
| txnpush | Read-write or write-write conflicts requiring timestamp pushes. Often from concurrent transactions on overlapping key ranges. | Concurrent transaction patterns and query isolation |
| commitdeadlineexceeded | Long-running transactions whose commit deadline expires before completion. | Transaction duration and kv.closed_timestamp.target_duration setting |
Quick checks
These commands assume localhost:8080 is the CockroachDB Admin UI / Prometheus endpoint. For secure clusters, add --cert flags as appropriate.
# Aggregate retry rate and all sub-metrics
curl -s http://localhost:8080/_status/vars | grep 'txn_restarts'
# Clock offset on this node relative to peers
curl -s http://localhost:8080/_status/vars | grep 'clock_offset'
# Intent accumulation (correlates with contention-driven retries)
curl -s http://localhost:8080/_status/vars | grep -E 'intentcount|intentbytes'
# NTP synchronization status
chronyc tracking
# Transaction commit and abort counts for rate calculation
curl -s http://localhost:8080/_status/vars | grep -E 'sql_txn_commit_count|sql_txn_abort_count'
-- Find the most recent contention events with blocking transaction details
SELECT * FROM crdb_internal.transaction_contention_events
ORDER BY contention_time DESC LIMIT 50;
-- Identify transactions with the highest restart counts
SELECT * FROM crdb_internal.node_txn_stats ORDER BY restart_count DESC LIMIT 20;
Note: crdb_internal.transaction_contention_events requires admin privileges and performs an expensive cluster-wide RPC. Use for diagnosis only, not continuous monitoring.
How to diagnose it
Break down the retry rate by cause. Pull
txn_restartssub-metrics and identify which cause dominates. If multiple causes are elevated, focus on the largest contributor first.If writetooold dominates, check for hot ranges. Use the DB Console Hot Ranges page or query
crdb_internal.rangesordered byqueries_per_second. A range with 10x the average QPS indicates a hot spot. Check whether the affected table uses sequential primary keys (SERIAL, auto-increment, timestamp-prefixed).If readwithinuncertainty dominates, check clock synchronization. Query
clock_offset_meannanosfor all node pairs. Any node above 50% of--max-offset(default 500ms, so above 250ms) is a problem. Runchronyc trackingorntpstaton all nodes. This cause does not occur for other reasons in meaningful quantities.If serializable dominates, inspect contention events. Query
crdb_internal.transaction_contention_eventsto find which transactions are blocking each other. Look for the blocking transaction fingerprint and the contended keys. This cause encompasses multiple sub-scenarios that the metric alone cannot distinguish.If txnaborted dominates, look for deadlocks. The application is likely acquiring locks in reverse order across concurrent transactions. Check whether the same tables are accessed in different orders by different transaction paths.
If txnpush dominates, review application transaction patterns. Look for transactions that read then write the same keys concurrently.
SELECT FOR UPDATEcan convert read-write conflicts into explicit locks that serialize more predictably.Correlate with intent accumulation. Check
intentcountandintentbytes. Growing intent counts mean abandoned or long-running transactions are leaving unresolved write intents. Other transactions encountering these intents may need to restart.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
txn_restarts by cause | Each cause maps to a different layer and requires a different fix | Any cause exceeding its baseline or a new cause appearing |
clock_offset_meannanos | Directly causes readwithinuncertainty restarts | Any node above 50% of --max-offset |
intentcount, intentbytes | Unresolved intents cause contention and restarts | Growing monotonically without corresponding active transaction increase |
| SQL P99 latency | Retries add directly to tail latency | P99 rising while P50 is stable (bimodal from retry overhead) |
txn_durations P99 | Transaction commit latency includes retry time | Ratio of txn latency to statement latency diverging from 1.0 |
| Per-range QPS | Identifies hot ranges driving writetooold restarts | Any range at 10x average QPS |
| SQL error rate (40001) | Serialization failures visible to applications | Sustained nonzero rate in production |
sql_txn_abort_count | Transactions aborted entirely, not just restarted | Rate increasing |
Fixes
writetooold: reduce write contention
Short-term: Use ALTER TABLE ... SPLIT AT to manually split hot ranges at known boundaries. This distributes traffic across more leaseholders. This is a schema operation that takes effect immediately.
Long-term: Redesign primary keys to use hash-prefixed or UUID-based values instead of sequential patterns. For counter-like patterns, consider application-side batching.
Query-level: Use SELECT ... FOR UPDATE to explicitly lock rows before updating, converting implicit conflicts into explicit locks. This can reduce restart overhead when multiple transactions compete for the same keys.
readwithinuncertainty: fix clock synchronization
Fix NTP on all affected nodes. Use cloud-specific time services where available (AWS Time Sync Service, Google Cloud time). After fixing NTP, monitor clock_offset_meannanos convergence. Time correction may be gradual (slew), not instantaneous.
If you trust your NTP infrastructure and still see persistent low-level readwithinuncertainty restarts, reducing --max-offset tightens the uncertainty window. This is a tradeoff: better read performance but higher risk of node self-termination if clock drift spikes. Changing --max-offset requires a rolling restart of all nodes.
See CockroachDB clock_offset_meannanos high: catching clock drift before self-termination and CockroachDB clock skew cascade: how shared NTP drift causes quorum loss for deeper coverage.
serializable: reduce transaction scope and conflicts
Identify the blocking transaction from crdb_internal.transaction_contention_events. Common fixes include:
- Shrinking transaction scope (fewer statements, shorter duration)
- Adding indexes to reduce full-table scans that hold locks
- Using historical reads (
AS OF SYSTEM TIME) for read-only parts of a transaction to avoid conflicts
For long-running transactions hitting the closed timestamp, either reduce transaction duration or increase kv.closed_timestamp.target_duration. The latter has tradeoffs for follower reads and CDC latency.
txnaborted: fix lock ordering
Identify which transactions are deadlocking by checking which keys they hold intents on in reverse order. Fix the application to acquire locks in a consistent order across all transaction paths. In practice, this means ensuring all code paths access the same tables and rows in the same sequence.
txnpush: reduce transaction conflict surface
Review application transaction patterns for concurrent read-then-write cycles on overlapping keys. Options include:
- Adding
SELECT ... FOR UPDATEto serialize access to contested keys explicitly - Reducing transaction width (fewer keys touched per transaction)
- Scheduling conflicting workloads to avoid peak concurrency windows
Prevention
Monitor by cause, not just total. Alert on individual restart causes with different thresholds. readwithinuncertainty should alert at any sustained nonzero rate. writetooold and txnpush can tolerate higher rates but should be trended. A sudden shift in the cause distribution is itself a signal.
Track retry rate as a fraction of total transactions. The absolute count matters less than the ratio. Use sql_txn_commit_count as the denominator. Alert when the ratio exceeds 10% sustained.
Watch for gradual increases. A retry rate that creeps from 1% to 3% to 5% over weeks indicates worsening contention. The system feels fine until it cascades. Trend the per-cause rates, not just the total.
Instrument application-side retries. The txn_restarts metric captures CockroachDB’s internal restarts, including transparent automatic retries. Application-side retries (reconnect and re-execute after receiving 40001) are not counted. The true contention cost may be higher than the database reports.
Review schema for sequential patterns. Any monotonic primary key (SERIAL, auto-increment, timestamp-prefixed) is a future hot range. Address these proactively before traffic grows.
How Netdata helps
- Per-second collection captures transient retry spikes that 15-30 second scrape intervals miss. Brief contention bursts that cause application-visible 40001 errors can appear and disappear between coarser scrapes.
txn_restartssub-metrics collected independently means you see each cause as its own time series without manual PromQL or separate alert rules.- Correlation between restart causes and clock offset lets you confirm or rule out NTP as the source of
readwithinuncertaintyrestarts within seconds. - Anomaly detection per cause flags distribution shifts even when the total rate has not crossed a fixed threshold. A first appearance of
readwithinuncertaintywhere it was previously zero is an early signal. - Correlation with intent count, SQL P99 latency, and per-node CPU helps distinguish contention-driven retries from overload-driven retries during an active incident.
See CockroachDB monitoring with Netdata for per-second metrics and anomaly detection.
Related guides
- CockroachDB clock_offset_meannanos high: catching clock drift before self-termination
- CockroachDB clock skew cascade: how shared NTP drift causes quorum loss
- CockroachDB clock synchronization error: this node is more than 500ms away from at least half of the known nodes
- CockroachDB compaction backlog growing: when Pebble can’t keep pace with writes
- CockroachDB context deadline exceeded: timeouts, slow ranges, and overload
- CockroachDB detecting hot ranges: per-range QPS, CPU asymmetry, and the Hot Ranges page
- CockroachDB hot range bottleneck: one leaseholder saturated while the cluster idles
- CockroachDB storage_l0_sublevels climbing: the earliest warning of write stalls
- CockroachDB LSM compaction death spiral: L0 sublevels, read amplification, and write stalls
- CockroachDB monitoring checklist: the signals every production cluster needs
- CockroachDB monitoring maturity model: from survival to expert
- CockroachDB node liveness failure: heartbeats, lease redistribution, and flapping






