Incident Runbooks
Operational procedures for common failure scenarios.
NOTE
Last Verified: June 2026. Runbooks should be tested quarterly against staging to ensure commands and metrics are still valid.
PostgreSQL Failure
Severity: P1 — Causes API 500s and blocks new job submissions.
Symptoms
Dispatcher connection losterrors in logslistener_reconnects_total{reason="postgres"}metric spike- API returning 500 errors
Actions
- Check DB Status:
docker psto verifyosurender-postgresis running - View Logs:
docker logs --tail 100 osurender-postgres— look for OOM kills, corruptions, or max connections - Connections: If max connections reached, restart API instances to flush pools, or increase
max_connectionsindocker-compose.yml(default: 200) - Recovery: The Dispatcher automatically reconnects using exponential backoff with jitter (3-10s). No manual restart required.
Worker Failure (Celery/Modal)
Severity: P2 — Degrades or halts render throughput, but API remains available to accept jobs.
Symptoms
active_render_workersdrops to 0 whilequeue_depth > 0NoActiveWorkersalert triggerszombie_jobs_reaped_totalincreasing
Actions
- Check Celery:
docker logs --tail 100 osurender-worker - Check Modal: Log into Modal dashboard → verify
osurender-gpu-workerstatus - Queue Health: If tasks queued but not processing, check Redis. Do NOT use
FLUSHALL. Clear only the Celery queue:docker-compose exec redis redis-cli DEL celery - Recovery: Failed jobs are automatically recovered by
reap_zombie_jobs(runs every 60s) and retried up to 3 times.
Storage Failure (R2/MinIO)
Severity: P2 — Prevents video upload/download, stalling the pipeline.
Symptoms
StorageFailuresalert triggersstorage_failures_totalspikes- API 500 errors on job submission
- Workers aborting during download phase
Actions
- Credentials: Verify
STORAGE_ACCESS_KEYandSTORAGE_SECRET_KEYin.env - Network: Ensure worker containers can resolve
STORAGE_ENDPOINT - Provider Status: Check Cloudflare Status for R2 issues
- Recovery: Jobs that fail to download are marked FAILED. Replay using the DLQ script once storage is restored.
Queue Saturation
Severity: P3 — Increased latency for users, potential API 503s if MAX_QUEUED is reached.
Symptoms
QueueDepthHighorQueueDepthCriticalalerts- API returning 503 Service Unavailable
Actions
- Scale Workers: Increase Celery concurrency:
-c 4indocker-compose.yml, ordocker-compose up -d --scale worker=3 - Modal Limits: Check if your Modal account hit concurrent invocation limits. Scaling local workers past Modal's concurrency ceiling will not improve throughput and may cause timeouts.
- Abuse Detection: Check Grafana for submission spikes from a single IP. May need Cloudflare WAF rules.
- Increase Capacity: Raise
MAX_QUEUEDin configuration if infrastructure can handle it.
Cloudflare Failure / IP Spoofing
Severity: P2 — Rate limiting bypass, potential resource exhaustion.
Symptoms
- Unusually high traffic bypassing rate limits
- Valid requests getting rate limited because
CF-Connecting-IPis missing
Actions
- Origin Shield: Verify firewall only allows traffic from Cloudflare IP ranges
- Headers: Ensure
CF-Connecting-IPis trusted. If bypassed, attacker is hitting origin directly.
Dead Letter Queue Growing
Severity: P3 — Background failures accumulating, requires manual intervention eventually but no immediate customer impact.
Symptoms
DeadLetterQueueGrowingalert- Events stuck in
FAILEDstatus
Actions
- Inspect: Query PostgreSQL for failed events:sql
SELECT id, event_type, last_error, created_at FROM outbox_events WHERE status = 'FAILED'; - Replay: Run
python scripts/replay_dead_letters.pyto reset failed events to PENDING - Root Cause: Check
last_errorfor patterns — usually Redis connectivity or Celery task failures
osu! API Failure / Rate Limiting
Severity: P2 — Prevents new beatmap/replay metadata lookups. Existing renders for cached beatmaps may still succeed.
Symptoms
- 429 Too Many Requests or 503 Service Unavailable errors in API logs during beatmap lookup.
- External API latency metrics spiking for
osu.ppy.sh.
Actions
- Verify Outage: Check the osu! server status page.
- Rate Limits: If hitting 429s, ensure your
OSU_API_KEYis valid and you aren't leaking keys. - Recovery: This is a third-party dependency. Recovery is time-based. Once the API recovers, failed jobs in the DLQ can be replayed.
Modal Cold Start
Severity: None — Expected behavior, but can trigger false-positive alerts.
Symptoms
active_render_workersdrops to 0 at low traffic periods (e.g., 3am).- The first job submitted after a quiet period takes 30-60s longer to begin rendering.
Actions
- Wait and Verify: Do not immediately restart services. Wait 60 seconds and check if the job transitions to
PROCESSING. - Check Logs: Modal logs will show container initialization taking place.
- Prevention: If latency is critical, consider implementing a keep-alive ping to Modal to maintain a warm container, though this incurs constant billing.
