Day 52 of 100·Project Deep-Dive Day·519 words

The notification lease and retry+backoff engineering

How traverse's 120-second lease works in every failure mode, and the retry ladder you own.

Know your own system

The deep version of your best lease implementation — src/queues/notification.queue.ts.

The claim SQL (be able to write it)

UPDATE notification_delivery nd
SET attempts = attempts + 1,
    next_attempt_at = NOW() + INTERVAL '120 seconds'   -- the lease
FROM (
  SELECT id FROM notification_delivery
  WHERE status='pending' AND next_attempt_at <= NOW()
  ORDER BY next_attempt_at
  LIMIT 50                                  -- DRAIN_BATCH_SIZE
  FOR UPDATE SKIP LOCKED                    -- competing drains claim disjoint rows
) claimed
WHERE nd.id = claimed.id
RETURNING nd.*;

Every design element has a reason: SKIP LOCKED = parallel drains; the lease is forward-dated, not a status = crashed workers need no reaper (Day 32); ORDER BY next_attempt_at = oldest-first fairness; batch 50 = bounded send burst per 30 s drain tick.

Failure-mode walkthrough (the interview drill)

  • Worker dies before sending: lease expires in ≤120 s; next drain re-claims; attempts incremented; eventually failed after MAX_ATTEMPTS=5. Cost: 2 minutes delay.
  • Worker alive but slow (APNs takes >120 s): lease expires while send is in flight; another drain claims and double-sends; both record sent. Accepted: push is collapse-coalesced; and the design doc says quiet-hours defer, never drop — same philosophy: err toward delivering.
  • APNs 5xx: failed(retryable) → backoff min(60s·2^(attempts-1), 1h) written as next next_attempt_at → status stays pending. Exponential with cap — no infinite growth.
  • APNs 410/Unregistered/BadDeviceToken: invalid_token → terminal; the PushToken is invalidated; no retry — error classification again (Day 19).
  • No token registered: skipped(no_push_token) — terminal, and not an error: "a decision, not a failure." The drain must never re-add skipped rows to the retry set — the code documents this.
  • APNs not configured (isApnsConfigured()==false): every push → skipped(unconfigured) — deliberate, terminal. Consequence stated in the design: configuring APNs later pushes only new notifications, never a backlog — because the rows already went terminal. That's a real tradeoff decision documented in the code header; recite it.

The retry ladder inventory (both projects)

LayerPolicyFile
zlink connector3 attempts, 500 ms base, exp+jitter, 30 s capzlink/internal/retry
zoth consumer5 attempts via republish + x-retry-count, then DLQinternal/queue/consumer
BullMQ submission3 attempts, exp 2 ssrc/queues/submission.queue.ts
BullMQ AI2 attempts, exp 5 ssrc/queues/aiAnalysis.queue.ts
BullMQ notification3 attempts, exp 2 s (job-level) + 5 delivery attempts with 60s·2^n→1h (row-level)src/queues/notification.queue.ts
Relay publishno retry — abort batch, retry next tick (outbox holds)internal/queue/outbox

Note the two-tier retry in notifications: the job can fail (BullMQ retries 3×), and separately the delivery row accumulates its own attempts with its own backoff — the row is the durable retry state; the job is just the trigger. That's the outbox-style separation of state from execution, and it's why the system is resumable from any crash point.

The jitter rule (everywhere or nowhere): zlink's delay/2 + rand(delay/2) — without jitter, synchronized failures (e.g., APNs blip at a drain tick) produce synchronized retries; with it, the herd spreads. BullMQ's exponential backoff includes jitter by default. Your consumer republish path has no jitter — an improvement to name (sleep-before-republish or randomized delay header).

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

Why is the lease 120 seconds and not a 'sending' status?

A status needs a cleanup job: a crashed worker leaves rows stuck 'sending' forever. A forward-dated lease is self-cleaning — the row becomes claimable exactly when the lease lapses, and a retry uses the same code path as a first attempt. The 120 s value covers a normal APNs/FCM send with margin; the cost of a wrong-side choice is bounded (double-send, coalesced) versus the cost of a stuck status (silent non-delivery). Time-based claims trade exactness for liveness, and for push notifications liveness is the right trade.

02

What happens if APNs is configured a month after launch?

Nothing retroactive: deliveries that ran while unconfigured went to 'skipped' — terminal by design — so only notifications emitted after configuration get pushed. That's documented as a decision: treating 'no provider' as a permanent condition rather than a backlog, because an old backlog pushed months later would be noise, not information. If I wanted catch-up, the fix is a distinct 'deferred' status the drain re-evaluates — but the product answer was 'never deliver stale pushes.'

03

Your retries have jitter in zlink but not in the consumer republish path. Why fix that?

Because the republish path's failure mode is synchronized: a batch of events failing on the same downstream outage will all retry at the same cadence, multiplying load exactly when the dependency is recovering. Jitter (randomizing the republish delay) converts the herd into a spread. It's a one-line improvement I'd make — and mentioning it shows the principle is applied by rule, not by accident.