Runbook: Incident Response
Who to Contactโ
See OWNERS.md for full contact list.
| Role | Responsibility |
|---|---|
| Platform Lead | Overall incident ownership, escalation to Eusden |
| On-call Engineer | First responder, initial triage |
| Eusden | Production deploy authority, major architectural decisions |
Production deploys are restricted to Eusden. For incidents requiring a production rollback or hotfix deploy, Eusden must be looped in.
Severity Levelsโ
| Severity | Definition | Response Time |
|---|---|---|
| P0 | All clients unable to send/receive messages; auth down | Immediate (wake up) |
| P1 | Major feature broken for all orgs (queue down, billing broken) | 30 minutes |
| P2 | Feature broken for some orgs; degraded performance | 2 hours |
| P3 | Minor issue; workaround available | Next business day |
Signal โ severity mappings (#3990)โ
Explicit classifications for alerts that previously had no place in the matrix (the #3444 30k-row DLQ pileup sat unclassified for a week โ never again):
| Signal | Severity | Rationale |
|---|---|---|
DLQ pending (queued+retrying) over DLQ_ALERT_THRESHOLD for 30+ minutes | P1 | Expert-approved replies may not be reaching customers โ treat as an outbound delivery incident. Runbook: dlq-handling.md. |
DLQ dead-growth alert fired (DLQ_DEAD_GROWTH_ALERT, #3986) | P2 | Deliveries are terminally failing for some channel/org; retention + replay tooling bound the blast radius. Runbook: dlq-handling.md. |
| Per-channel canary failure (#3989, once live) | P1 | A whole outbound channel is broken โ the #3506 class. |
First Response Checklistโ
Where prod runs (2026-06-02): Production API + agent run on AWS ECS Fargate (#1101, Terraform modules under
infra/terraform/modules/{ecs-cluster,ecs-service,alb,monitoring}). Frontend stays on Vercel. Legacy Railway services remain reachable for dev/staging during the migration window. Each section below leads with the ECS / AWS path; the Railway block beneath is the legacy / staging fallback โ copy from the correct block for the env you're paged on. When the failing thing is AWS infrastructure itself (a whole ECS service down, ALB 5xx surge, RDS failover, RDS Proxy saturation, a CloudWatch alarm), use the deep-dive companionaws-outage.md.
-
Check the live control plane.
ECS / AWS (primary prod path): commands use
humanwork-<env>โ substitutehumanwork-prodorhumanwork-staging(the Terraformname_prefix). The ECS cluster ishumanwork-<env>-cluster, services/task-def families arehumanwork-<env>-api/humanwork-<env>-agent, and log groups are/ecs/humanwork-<env>-api//ecs/humanwork-<env>-agent.aws ecs describe-services --cluster humanwork-<env>-cluster \
--services humanwork-<env>-api humanwork-<env>-agent \
--query 'services[].{Name:serviceName,Desired:desiredCount,Running:runningCount,Status:status}'
aws logs tail /ecs/humanwork-<env>-api --follow --since 15m
aws logs tail /ecs/humanwork-<env>-agent --follow --since 15mAlso: CloudWatch dashboards (
humanwork-<env>-overview) and the ALB target group health.Railway (legacy / staging fallback):
railway logs --service humanwork-api | tail -50
railway logs --service humanwork-agent | tail -50 -
Check health endpoints:
curl https://api.h.work/health
curl https://api.h.work/ready
# If the custom domain is down, hit the ALB DNS for the ECS service directly
# (look it up in the ALB console or `aws elbv2 describe-load-balancers --query 'LoadBalancers[].DNSName'`):
curl https://<alb-dns-name>/health -
Check Sentry for a spike in errors (#845). Filter by
environment:productionand the last 30 minutes. Bothhumanwork-apiandhumanwork-agentprojects carryorg_id+osa_idtags โ group byorg_idto see if the impact is one tenant or platform-wide. -
Check leader-status (#865/#1007) โ multi-pod deployments rely on a single pod holding the Redis leader lease for scheduled jobs:
curl -H "Authorization: Bearer ${SUPERADMIN_TOKEN}" \
https://api.h.work/ops/leader-statusNo leader for > 60s means cron sweeps + Haystack reconciliation + billing rollups are not running. See
docs/runbooks/observability.mdยง Leader-Status. -
Check Vercel dashboard for frontend deploy status
-
Check PostgreSQL connectivity:
GET /readywill fail if DB is unreachable -
Check
ThrottlerModuleshadow-mode alerts in Sentry (#812/#839) โ sustainedthrottler.shadow_blockspikes may indicate an abuse storm even though no requests are being blocked yet. Seedocs/runbooks/observability.mdยง Throttler Shadow Mode. -
Post status update in team Slack channel within 15 minutes of incident start
API / Agent Service Downโ
Symptoms: GET /health returns non-200; the live control plane shows tasks crashed or restarting.
ECS / AWS path (primary prod)โ
- Check service health + recent task stops:
aws ecs describe-services --cluster humanwork-<env>-cluster --services humanwork-<env>-api \
--query 'services[0].{Desired:desiredCount,Running:runningCount,Pending:pendingCount,Events:events[0:5]}'
aws ecs list-tasks --cluster humanwork-<env>-cluster --service-name humanwork-<env>-api --desired-status STOPPED
aws ecs describe-tasks --cluster humanwork-<env>-cluster --tasks <task-arn> - Tail logs:
aws logs tail /ecs/humanwork-<env>-api --follow --since 15m - Look for: OOM (Out of Memory โ also see
OOMKilledEventBridge rule in observability.md), migration failure, missing env var/secret, crash on startup. - OOM: increase
memory/memoryReservationin the task definition (infra/terraform/modules/ecs-service) and re-apply; or temporarily scale out (aws ecs update-service --desired-count). - Migration failure: check
DATABASE_URL(RDS Proxy endpoint) andDATABASE_SSL=truein the task definition / AWS Secrets Manager; run the migration manually viaaws ecs execute-commandinto a running task. - Env var / secret missing: update Secrets Manager or the task-definition
environmentblock; force a new deployment (aws ecs update-service --force-new-deployment). - Force a restart:
aws ecs update-service --cluster humanwork-<env>-cluster --service humanwork-<env>-api --force-new-deployment. - Rollback: see
## Rollback Procedurebelow.
Railway path (legacy / staging fallback)โ
- Check logs:
railway logs --service humanwork-api --lines 100 - Look for: OOM, migration failure, missing env var, crash on startup
- If OOM: scale up memory in Railway dashboard (Settings โ Resources)
- If migration failure: check
DATABASE_URLandDATABASE_SSL=trueare set; run migration manually - If env var missing: check Railway Variables for the service; compare against
.env.example - Restart service: Railway dashboard โ Service โ Deployments โ Restart
- If restart fails: rollback to previous deploy
Railway keeps the last 5 deployments.
railway rollback --service humanwork-api --deployment <previous-deploy-id>
Vercel Deploy Failed / Frontend Downโ
Symptoms: https://app.h.work (production) or https://humanwork-frontend.vercel.app returns 500 or shows stale build.
Steps:
- Check Vercel dashboard โ Deployments โ find failing deploy โ view build logs
- Common causes: TypeScript build error, missing env var, Next.js build timeout
- To rollback: Vercel dashboard โ Deployments โ select previous successful deploy โ "Promote to Production"
- To redeploy from CLI:
cd frontend && vercel --prod
Database Connection Errorsโ
Symptoms: GET /ready fails; API logs show ECONNREFUSED or connection timeout.
ECS / RDS path (primary prod)โ
- Check RDS instance + RDS Proxy status:
aws rds describe-db-instances --db-instance-identifier humanwork-<env>-pg \
--query 'DBInstances[0].{Status:DBInstanceStatus,Endpoint:Endpoint.Address}'
aws rds describe-db-proxies --db-proxy-name humanwork-<env>-pg-proxy - Verify the task definition's
DATABASE_URL(Secrets Manager) points at the RDS Proxy endpoint, not the raw instance, and thatDATABASE_SSL=trueis set. - Connection pool exhausted? Check
Database/Postgres/DatabaseConnectionsCloudWatch metric and the RDS ProxyClientConnections/DatabaseConnectionsmetrics. The proxy is what fans out client connections โ if it's saturated, scale or look for a leak. - If the DB itself is degraded: AWS Console โ RDS โ Events; consider failover (
aws rds reboot-db-instance --force-failoverfor Multi-AZ). For the full Multi-AZ failover and RDS Proxy degradation playbooks, seeaws-outage.mdยง RDS Multi-AZ Failover and ยง RDS Proxy Degradation. - After a restore, verify extensions:
psql "$DATABASE_URL" -c "\dx"
# Expected: vector, pg_trgm extensions present.
Railway path (dev only)โ
Dev only โ staging/prod are on AWS ECS Fargate (RDS Proxy). See
docs/ops/deploy.md.
- Check Railway Postgres service status in dashboard
- Verify
DATABASE_URLenv var is set on the API service - Verify
DATABASE_SSL=trueis set (Railway Postgres requires SSL) - Check if connection pool is exhausted (Railway Postgres has a connection limit per plan)
- Dev runs direct to Postgres (no pooler โ decision #1233, 2026-06-05). If pool exhaustion shows up in real usage on dev, restart the API service to release connections. Don't reflexively reintroduce PgBouncer โ RDS Proxy already handles pooling on staging/prod, and dev's load profile typically doesn't justify the extra moving part. If saturation becomes chronic on dev, the alternatives are (a) bump
DATABASE_POOL_MAX(default20, seerailway-env-vars.md) and the Postgresmax_connectionsceiling, (b) re-wire the unused Railway PgBouncer plugin, or (c) move dev to AWS.
- Dev runs direct to Postgres (no pooler โ decision #1233, 2026-06-05). If pool exhaustion shows up in real usage on dev, restart the API service to release connections. Don't reflexively reintroduce PgBouncer โ RDS Proxy already handles pooling on staging/prod, and dev's load profile typically doesn't justify the extra moving part. If saturation becomes chronic on dev, the alternatives are (a) bump
- If Postgres service is crashed: Railway dashboard โ
humanwork-postgresโ Restart - Verify extensions are installed after restore:
Expected:
railway run --service humanwork-postgres \
psql "$DATABASE_URL" -c "\dx"vector,pg_trgmextensions present.
Agent Errors Spikingโ
Symptoms: Expert queue filling up faster than usual; POST /chat or POST /v1/chat returning errors; API logs show agent timeouts.
ECS / AWS path (primary prod)โ
- Tail agent logs:
aws logs tail /ecs/humanwork-<env>-agent --follow --since 15m - Look for: OpenAI API rate limits (429), model errors (500), OOM.
- If OpenAI rate limit: check usage at platform.openai.com; reduce throughput or upgrade tier.
- If agent is OOM: bump task memory in the agent's ECS task definition (Terraform) and re-apply.
- To disable auto-send temporarily (route everything to Expert queue):
- Update the API task definition's env / Secrets Manager value: set
CONFIDENCE_THRESHOLD_OVERRIDE=200. (For one-shot incident control, AWS SSM Parameter Store also works if the task is wired to read from there.) - Force a new deployment:
aws ecs update-service --cluster humanwork-<env>-cluster --service humanwork-<env>-api --force-new-deployment. - This must be set higher than any per-org
autoRespondThreshold(default is 101 โ "never auto-send"; an override of 200 ensures no org can ever auto-send during the incident). - All responses will route to Expert queue regardless of agent confidence.
- Remove the override when agent is healthy.
- Update the API task definition's env / Secrets Manager value: set
- If agent tasks are repeatedly crashing, force a new deployment after fixing config:
aws ecs update-service --cluster humanwork-<env>-cluster --service humanwork-<env>-agent --force-new-deployment
Railway path (legacy / staging fallback)โ
- Check agent logs:
railway logs --service humanwork-agent | tail -100 - Look for: OpenAI API rate limits (429), model errors (500), OOM
- If OpenAI rate limit: check usage at platform.openai.com; reduce throughput or upgrade tier
- If agent is OOM: scale memory in Railway dashboard
- To disable auto-send temporarily (route everything to Expert queue):
- In Railway Variables for
humanwork-api, setCONFIDENCE_THRESHOLD_OVERRIDE=200 - This must be set higher than any per-org
autoRespondThreshold(default is 101 โ "never auto-send"; an override of 200 ensures no org can ever auto-send during the incident). - All responses will route to Expert queue regardless of agent confidence.
- Remove the override when agent is healthy.
- In Railway Variables for
- If agent service is repeatedly crashing, restart it:
railway up --service humanwork-agent
WhatsApp (Twilio) Not Receiving Messagesโ
Symptoms: WhatsApp messages not arriving in API; clients reporting messages to Specialist not getting responses.
Steps:
- Check API logs for Twilio webhook activity (
aws logs tail /ecs/humanwork-<env>-api --follow --since 30m | grep whatsappon ECS;railway logs --service humanwork-api | grep whatsapp | tail -50on Railway-legacy). - Check Twilio Console โ Monitor โ Errors for webhook delivery failures.
- Verify webhook URL in Twilio Console matches
https://api.h.work/channels/whatsapp/inbound(prod) or the dev URL for the env. - Verify
TWILIO_AUTH_TOKEN(main account) โ on ECS this lives in AWS Secrets Manager; on Railway-legacy in Railway Variables. For subaccount-owned senders, also checkTWILIO_SUBACCOUNT_TOKEN. - If webhook is being rejected with 403:
TWILIO_AUTH_TOKENorTWILIO_WEBHOOK_BASE_URLis wrong โ update the corresponding secret store and force a new deployment. - Twilio retries failed webhooks for up to 4 hours โ messages are not lost unless the outage exceeds that window.
Expert Queue Not Delivering Notificationsโ
Symptoms: New queue items not appearing in real-time; experts must refresh to see new items.
Steps:
- Check if WebSocket connection is established: browser DevTools โ Network โ WS tab
- Check API logs for Socket.io errors
- Check
NEXT_PUBLIC_WS_URLenv var is set correctly in Vercel (should bewss://api.h.workfor production, or the ALB DNS / staging hostname if the custom domain is down). - Verify CORS:
ALLOWED_ORIGINSenv var on API must include the Vercel frontend domain (e.g.https://humanwork-frontend.vercel.apporhttps://app.h.work) - If Socket.io failing, queue still functions โ experts can poll manually. Not a P0.
Feature Flags / Kill-Switchesโ
When to use: a Specialist capability is actively causing harm (bad KB results, runaway tool calls, an agentic loop, or auto-replies going out that shouldn't) and you need to stop the bleeding faster than a deploy or rollback. Flipping a flag takes effect across the fleet within ~15 s (the in-process resolve cache TTL) โ no redeploy required.
Full system reference: docs/features/feature-flags.md.
Surface: SuperAdmin โ /ops/feature-flags. Pick the narrowest scope that
contains the blast radius โ a single (Org ร Specialist) override first, then the
whole org, and only global when the problem is platform-wide.
| Symptom | Flag to flip OFF | Effect |
|---|---|---|
| KB returning wrong/leaked context, or Haystack misbehaving | kb_retrieval_enabled | Specialist drafts with no KB context (empty retrieval), keeps responding. |
| Runaway / erroring tool calls (Shopify/Amazon, etc.) | tool_calling_enabled | Tool calls blocked at the gateway (status=blocked), drafting continues without tools. |
| Agentic task loop or bad open-ended drafting | agentic_mode_enabled | Agentic task creation returns 403; pipeline falls back to non-agentic drafting. |
| AI drafts being auto-delivered to clients that shouldn't be | auto_reply_enabled | All drafts queue for Expert review (always-HITL); nothing auto-sends. |
Procedure:
- Open
/ops/feature-flags(SuperAdmin). If you only need to contain one client, use Add override in Per-org overrides (Org ID) or Per-org-specialist overrides (OSA id) instead of touching the global row. - Toggle the flag OFF. A reason is mandatory โ write the incident id / short cause; it is recorded in the per-flag audit trail and the platform audit log.
- Confirm propagation: allow up to ~15 s, then verify the symptom stops. A
narrower override beats a broader one, so an
org = ONrow will keep a client live even whileglobal = OFF(and vice-versa) โ check you flipped the scope that actually wins. - Recovery: once the underlying fix is deployed, flip the flag back ON (or delete the override) with a reason. Use the History (clock) icon to see who changed what and confirm the live state.
Flipping a flag is a mitigation, not a fix โ still file the post-incident issue and roll out the real change. Remember
auto_reply_enableddefaults OFF and most others default ON; re-read the live row rather than assuming.
Rollback Procedureโ
ECS / AWS path (primary prod)โ
ECS rollback = re-point the service at the previous task-definition revision. ECS keeps task-definition history indefinitely; the service-events list shows the last several deploys.
# List recent task-definition revisions for the API
aws ecs list-task-definitions --family-prefix humanwork-<env>-api --sort DESC --max-items 10
# Roll the service back to the prior revision
aws ecs update-service \
--cluster humanwork-<env>-cluster \
--service humanwork-<env>-api \
--task-definition humanwork-<env>-api:<prev-revision-number>
# Watch the rollout
aws ecs describe-services --cluster humanwork-<env>-cluster --services humanwork-<env>-api \
--query 'services[0].deployments'
Frontend rollback is unchanged: Vercel dashboard โ Deployments โ previous successful deploy โ "Promote to Production".
Railway path (legacy / staging fallback)โ
# Railway: list recent deployments
railway deployments --service humanwork-api
# Rollback to a specific deployment
railway rollback --service humanwork-api --deployment <deploy-id>
# Vercel: promote a previous deployment
# Go to: vercel.com โ Project โ Deployments โ click previous deploy โ "Promote to Production"
Forensics โ reconstructing what happenedโ
Use the platform audit log at /ops/audit (SuperAdmin only) to reconstruct
the action timeline during and after an incident: who did what, to which
resource, in which client, and when. Filter by Client + time window, then
narrow by Action / Entity, and export to CSV to attach to the
post-mortem. Impersonation sessions also have a dedicated trail at
/ops/impersonation, and feature-flag flips are recorded in the per-flag
history (clock icon on /ops/feature-flags) as well as the platform audit log.
Caveat: audit writes are eventual / fire-and-forget (ADR-019) โ they happen
after the business commit and failures are swallowed, so treat the log as a
strong operational record, not a guaranteed-complete ledger. See
docs/features/audit-log.md for the event model,
scope, and retention notes.
Post-Incidentโ
- Write a brief post-mortem (timeline, root cause, fix, prevention) โ pull the
action timeline from
/ops/audit(see Forensics) - Create GitHub issues for any systemic gaps found
- Update this runbook if the incident revealed a missing procedure