docs/solutions/database-issues/seeder-auxiliary-redis-writes-timeout.md
seed-gdelt-intel was crashing with FATAL: The operation was aborted due to timeout during the post-fetch Redis write phase. The upstream GDELT fetch had already succeeded and the canonical key publish (atomicPublish) already retried transient failures, but the auxiliary timeline-key writes and TTL extensions in afterPublish used one-shot fetch() calls. A single Upstash latency spike turned a transient blip into a full seeder crash and a Railway "Deploy Crashed!" email.
FATAL: The operation was aborted due to timeout appears after Extended TTL on N key(s) / WARNING: N key(s) were expired/missing logs.PUBLISH_TIMEOUT with a warning severity.atomicPublish already wrapped its staging/canonical SET/DEL in withRetry, but that only protects the canonical key. The afterPublish auxiliary writes (writeExtraKey, extendExistingTtl) were left single-shot.extendExistingTtl. That helper already caught errors and returned false, but writeExtraKey threw on any non-ok response or abort, and neither helper retried — so a transient timeout still failed the run.Wrap both auxiliary Redis helpers in the same retry contract already used by redisCommand and atomicPublish:
writeExtraKey (scripts/_seed-utils.mjs:668) now wraps its SET call in withRetry with 2 retries and a 1s base delay.extendExistingTtl (scripts/_seed-utils.mjs:722) now wraps its /pipeline call in withRetry with the same budget.nonRetryable so they fail fast.Retry-After header.The boolean contract of extendExistingTtl is preserved: it still returns true only when every EXPIRE returns 1. A successful response with some EXPIRE no-ops (missing/expired keys) is a real data condition, not a transient error, so it returns false without burning retries.
Fixed in PR #5364.
The root cause was not a bad source or bad data — it was a missing resilience layer on the auxiliary write path. Upstash REST is served over the public internet; a single stalled request or brief 503 is expected at scale. The canonical publish path already treated these as retryable; the auxiliary path did not. Adding retry makes the failure mode symmetric across all Redis writes in a seeder run.
scripts/_seed-utils.mjs, decide its retry contract up front. Helpers that write seeded data should default to withRetry unless the caller explicitly needs fail-fast semantics.redisCommand:
PERMANENT_4XX_STATUSES → err.nonRetryable = true429 → parse Retry-After into err.retryAfterMswithRetry back offwriteExtraKey and extendExistingTtl now cover timeout, 503, 429, and permanent 401 paths.diagnose-railway-seeders skill class PUBLISH_TIMEOUT — post-fetch Redis publish timed out.