H12 high
Heroku H12: Request timeout (30s)
A web dyno took longer than Heroku's hard 30-second limit to start responding, so the router killed the connection.
What you see
at=error code=H12 desc="Request timeout" method=GET path="/report" dyno=web.1 service=30000ms status=503
What’s actually happening
The client gets a 503 (or a truncated response) after exactly 30 seconds. Your dyno log shows H12 with service=30000ms. The dyno itself keeps running and may finish the work seconds later, but the router already returned the connection because the first byte never arrived in time. Slow database queries, third-party API calls, and report generation are the usual triggers.
Common causes
- A synchronous request does real work — PDF/report generation, big exports, image processing — past 30s
- A slow or blocked downstream call (external API, an unindexed DB query) stalls the response
- No streaming: the app buffers the entire response and sends nothing within the window
- Thread/worker pool saturated, so requests queue and exceed 30s before they even start
- The 30-second router limit is fixed and cannot be raised — only the first-byte deadline matters, then a 55s rolling window after that
How to fix it
- Move the slow work to a background jobOffload anything heavy to a worker dyno via Sidekiq, Resque, or Celery. Return 202 immediately with a job ID, then let the client poll or get notified when it's done. This is the canonical H12 fix — the web request returns in milliseconds.
- Find what's actually slowIn the Heroku logs match the H12 path to your APM (New Relic/Scout) or add timing logs. Usually it's one N+1 query or a blocking external call. Fix the query with an index, or wrap external calls in a short client-side timeout (5-10s) so they fail fast instead of hanging to 30s.
- Stream the response to beat the first-byte deadlineH12 fires when no bytes arrive within 30s. If you can start emitting output early (chunked/streaming response), the timer resets to the 55-second rolling window. Useful for large downloads you can't fully precompute.
- Scale workers and tune the serverIf requests queue behind a saturated pool, add dynos or raise web concurrency (puma threads, gunicorn workers). Set the app server's own timeout (e.g. Puma worker timeout) below 30s so a stuck request is recycled rather than counted against the router.
Stop it recurring
Treat 30 seconds as a hard ceiling in design: any endpoint that might exceed a few seconds should return a job handle, not do the work inline.
Related errors