What happens on failures and at high volume: the module does not lose data and survives crashes. Any sync is a background job on SAQ over Redis, not a request within an HTTP session. The administrator starts a sync and can close the tab: no connection needs to be held, breaking the session does not stop the work — the run is carried by a worker. What follows is the resilience path of a single run: SyncRun → checkpoint → retries → DLQ → visibility. The modes themselves that drive these runs are on the sync modes page.
SyncRun — the single
source of truth for how the work is going. It holds the state
(queued / running /
succeeded / failed /
cancelled), the
“N of M processed” progress, the current checkpoint, the
worker's heartbeat, and accumulated errors. The UI computes
nothing itself: it reads the SyncRun and renders
progress and outcome from it.
SyncRun. A stopped heartbeat means the worker has
died (pod restart, container recreation): the run is picked back
up and continues from the last checkpoint. The heartbeat
interval is 30 s, and silence is counted as a crash after
three missed beats (90 s): enough to ride out a GC pause or
a brief pod restart, while a stuck run is reclaimed within a
minute or two. Both values are fixed worker constants, not a
source setting. A worker crash turns into a pause, not a loss.
The heartbeat beats from a separate coroutine, independent of
the retry stack:
otherwise a long pause inside an attempt would look like the
worker dying and trigger a false restart. A short backoff
(60 s cap) is safe — it stays below the 90 s silence
threshold; long pauses do not hold the worker at all, they go
into deferred re-enqueue.
cancelled) is terminal, its checkpoint is not
resumed — the next run starts from scratch. This way a
day-long source pause does not lead to loading a stale
selection. Resume applies only to a connector's incremental run
whose stream is globally ordered by modification time (a
manifest flag): with an unordered stream a watermark does not
guarantee everything earlier has been processed. Reconciliation
and a targeted DLQ retry do not resume and do not move the
cursor — their selection does not reflect the stream frontier.
429 or 5xx
from the source API — a reason to retry, not to fail. The
request is retried with backoff: the pause between attempts
grows, so as not to overload the source and to wait out a spike.
After the attempts are exhausted, the item is not lost — it moves
further along the path, into the
DLQ.
429 · 500 · 502 ·
503 · 504 · 408 ·
network timeouts and drops
400 · 401 · 404 ·
422 · normalization and validation errors
403 on GitHub/GitLab: more often
a rate limit than an access denial, so before sending to the DLQ
the connector checks
Retry-After / X-RateLimit-Remaining and
treats such a 403 as transient.
Retry-After, a
degraded source) the worker does not sleep off —
it re-enqueues the job (SAQ defer / re-enqueue) and frees the
slot. This is how the
checkpoint freshness
budget
(6 hours) is realized through deferred re-enqueue, not by
holding a worker on sleep. If the source sent a
Retry-After, it
overrides the computed backoff.
succeeded with
error_count > 0 — finished
successfully, but not flawlessly: the error count separates a
partial failure from a total one (failed). The DLQ
is the durable table
dead_letters
in Postgres, not ephemeral run state: the backoff retries above
live in the worker job's memory and are measured in seconds,
while a failed item awaits manual triage and survives a worker
restart.
DLQ
SyncRun shows “N/M processed, K in DLQ” — it reveals
both the progress and the outcome of any run.
SyncRun data here.