Retries and Idempotency
This guide explains how retries work in the SDK and when to enable idempotency.
Retry policy overview
Retries are powered by @ydbjs/retry
with sensible defaults:
- Immediate retry for
BAD_SESSION
,SESSION_EXPIRED
,ABORTED
. - Exponential backoff for
OVERLOADED
and gRPCRESOURCE_EXHAUSTED
(starts at 1000 ms). - Exponential backoff for all other retryable cases (starts at 10 ms).
- Budget is unlimited by default; pass
budget
to cap attempts.
Query retries depend on the idempotency flag:
- Always retried:
ABORTED
,OVERLOADED
,UNAVAILABLE
,BAD_SESSION
,SESSION_BUSY
. - Conditionally retried (only with
.idempotent(true)
):SESSION_EXPIRED
,UNDETERMINED
,TIMEOUT
.
See implementation: packages/retry/src/index.ts
and packages/query/src/query.ts
.
Marking single calls as idempotent
ts
await sql`UPDATE counters SET v = v + 1 WHERE id = ${id}`
.idempotent(true)
.timeout(3000)
Inside sql.begin
/sql.transaction
, the per-call idempotency flag is ignored; configure idempotency at the transaction level and make your business logic idempotent (e.g., via idempotency keys).
Customizing retry strategy
ts
import { retry, defaultRetryConfig, strategies } from '@ydbjs/retry'
await retry({
...defaultRetryConfig,
budget: 5,
strategy: strategies.exponential(200),
}, async (signal) => {
return await sql`SELECT 1`.signal(signal)
})
Topic streaming
Topic readers/writers reconnect on failures and rebuild command queues. Use retryConfig
on writer for fine tuning; keep producers idempotent with producerId + seqNo
.
Best practices
- Prefer idempotent operations and use idempotency keys for at-least-once flows.
- Set explicit timeouts to constrain tail latencies.
- Log retries via
on('retry')
and enableDEBUG=ydbjs:*
in staging.