CockroachDB exposes the automatic retries behind successful requests
CockroachDB 25.2.3 counts automatic SQL transaction and statement retries separately from retryable storage errors. The distinction makes successful requests with repeated execution easier to investigate.
A successful SQL request can contain more execution work than the application sees. CockroachDB can encounter a retryable error, repeat execution internally and return a successful result. The application records no failed transaction, even though contention has consumed time and resources. The July 28 release of CockroachDB 25.2.3 adds counters for those automatic retries at the SQL layer.
The distinction matters because existing retry metrics describe a different event. A retryable error emitted by the storage layer and an automatic retry performed by the SQL layer are related, but they are not interchangeable measurements. Counting both provides a clearer account of what happened between the first execution attempt and the response.
I value CockroachDB's default serializable isolation because it makes the consistency contract explicit. That contract still has an execution cost. An internal retry belongs in the explanation of request latency even when it saves the application from handling an error.
Count the action as well as the error
The release introduces sql.txn.auto_retry.count for automatic SQL transaction retries and sql.statements.auto_retry.count for automatic SQL statement retries. Its notes explicitly distinguish these counters from txn.restarts.*, which count retryable errors emitted by the KV layer. The new counters measure retry actions taken by SQL in response to some of those errors.
That wording rules out interpreting either counter as a count of failed application requests. An automatic retry can be part of an eventually successful request. Conversely, an error that requires application involvement does not become an automatic SQL retry merely because its error code indicates that execution can be attempted again.
The transaction retry error reference explains the application boundary. CockroachDB attempts internal retries where possible. When it cannot complete that work internally, a retryable failure reaches the client with SQLSTATE 40001, and the application must handle the transaction retry. Client error counts therefore capture only the retries that crossed that boundary.
For operational interpretation, the new metrics fill a specific gap. If application error rates remain low while automatic retries increase, successful requests may still be doing repeated work. That is a reason to investigate concurrency and execution behavior, rather than evidence that a particular query or storage node has already been identified as the cause.
Result delivery limits transparent replay
Automatic retry is constrained by the interaction between SQL execution and the client protocol. The transaction documentation describes transparent retries for individual statements and transactions submitted as a single batch, subject to result buffering. The documented default buffer is 16 KiB, including protocol overhead.
While results remain buffered, the server can discard an unsuccessful attempt without the application having acted on its output. Once results have been streamed to the client, replay becomes a different problem. The client might already have consumed rows or used them to choose its next statement. Repeating execution behind that client's back cannot generally recreate the same conversation.
The batch requirement also has a concrete protocol meaning. Sending a complete group of statements together differs from issuing one statement, reading its result and constructing the next request. The latter involves application decisions between executions. A transaction's SQL text alone therefore does not establish whether CockroachDB can retry it automatically.
This helps explain why retry behavior can differ between superficially similar workloads. Result size and request grouping affect the server's ability to hide a retry. A count of internally repeated transactions needs that execution context before it can be compared meaningfully with client-visible serialization failures.
Statement retries have their own controls
Version 25.2.3 also adds initial_retry_backoff_for_read_committed. The release describes it as the initial backoff for retrying an individual statement in an explicit READ COMMITTED transaction. Setting it to zero disables exponential backoff. This setting concerns the statement retry loop, which is distinct from replaying an entire serializable transaction.
The READ COMMITTED documentation explains why the distinction exists. That isolation level permits behavior across statements that serializable isolation excludes. Retrying a statement under that contract does not promise the same transaction-wide result as restarting a serializable transaction.
The release notes identify a retry-limit error that can still escape this statement loop. For the documented 40001 message reporting that the read committed retry limit was exceeded, they suggest an initial backoff proportional to typical statement execution time, together with an increased max_retries_for_read_committed. Those are responses to a specific failure mode, rather than a universal recommendation to increase retry limits.
I find the separation useful because it keeps observability aligned with the isolation choice. A transaction retry and a statement retry repeat different units of work. Keeping their counters separate makes it possible to ask which unit is consuming extra attempts, while the backoff setting gives the statement loop a way to wait between attempts instead of immediately adding another conflicting execution.