Fix Heroku H12 Request Timeout in Django
Fix Heroku H12 Request timeout errors for Django: understand the 30-second router limit, find slow views, set Gunicorn timeouts below it, move long work to worker dynos with Celery or RQ, and avoid H13 cascades.
Problem statement
Some requests fail with Heroku's error page and the router logs:
heroku[router]: at=error code=H12 desc="Request timeout" method=POST path="/reports/generate/" service=30000ms status=503
Heroku's router gives every request 30 seconds to start returning a response. If your Django view has not sent response headers by then, the router returns a 503 to the client and logs H12. The dyno keeps working on the request, unaware that nobody is waiting, which ties up a Gunicorn worker and can make other requests queue and time out too.
- Find the slow paths: filter router logs for
H12and group bypath. - Set Gunicorn's
--timeoutbelow 30 (for example 25) so stuck workers are recycled instead of piling up. - Optimise slow views: query counts, indexes, external API timeouts.
- Move long tasks to a worker dyno with Celery or RQ, and return immediately.
- Watch for H13 and queueing: too few workers for traffic makes every request slower.
Step-by-step solution
1. Group H12s by path
heroku logs --num 1500 --app myapp | grep "code=H12" | grep -o 'path="[^"]*"' | sort | uniq -c | sort -rn
Usually one or two endpoints account for most timeouts: exports, report generation, file processing, or a view calling a slow third-party API.
2. Set Gunicorn's timeout below the router limit
web: gunicorn config.wsgi --timeout 25 --log-file -
Gunicorn's default 30-second timeout is equal to the router limit, so a worker can be stuck for the full 30 seconds after the router has already given up. With 25 seconds, Gunicorn kills and replaces stuck workers sooner and logs WORKER TIMEOUT, which points to the slow code. The rack-timeout equivalent for Python is exactly this setting.
3. Make the slow views faster
- Count queries with
django-debug-toolbarlocally or an APM in production; fix N+1 patterns withselect_relatedandprefetch_related. - Add indexes for filters used by the slow view.
- Set explicit timeouts on outbound HTTP calls (
requests.get(url, timeout=10)); a hanging API should fail in seconds, not consume the whole router budget. - Paginate large lists and API responses.
4. Move long work to a worker dyno
Anything that can legitimately take longer than a few seconds belongs in a background job:
web: gunicorn config.wsgi --timeout 25 --log-file -
worker: celery -A config worker -l info --concurrency 2
heroku addons:create heroku-redis:mini --app myapp
heroku ps:scale worker=1 --app myapp
The view creates a job, queues a task, and returns 202 Accepted with a status URL; the browser polls or receives an email when the result is ready. Store generated files in S3 rather than on the dyno filesystem, which is ephemeral and not shared between dynos.
5. Watch request queueing
If many requests are slow, Gunicorn workers fill up and new requests wait in the dyno's backlog, eventually also hitting H12 even though their own work is fast. Check heroku logs for service= values creeping up across all paths, and Heroku metrics for response time and dyno load. Increase WEB_CONCURRENCY if memory allows, or add dynos.
Verification check: after deploying, the H12 count from heroku logs | grep -c H12 over a busy hour should drop to zero, and slow paths should return 202 quickly with work completing in worker logs.
Explanation
Heroku's routing layer protects the platform and users from hung connections by enforcing a 30-second window for the first response byte, followed by a rolling 55-second window for each subsequent byte. The router cannot cancel work on the dyno, so a timed-out request keeps occupying a Gunicorn worker until it finishes or Gunicorn kills it. With few workers per dyno, a handful of slow requests can block everything, turning a single slow endpoint into site-wide H12s.
Edge cases / notes
- Streaming responses can exceed 30 seconds if they send the first byte in time and keep sending data at least every 55 seconds.
- File uploads count toward the window; upload large files directly to S3 with presigned URLs.
- H13 (connection closed without response) often accompanies H12 when Gunicorn kills a worker mid-request.
- Preboot avoids slow first requests after deploys, which can otherwise trigger H12s on cold starts.
Next steps
- If the dyno crashes instead, see Fix Heroku H10 App Crashed for Django.
- Find the slow code with Profile Slow Django Endpoints in Production.
FAQ
What is the Heroku request timeout?
30 seconds for the first response byte. It cannot be changed.
Does the request stop when H12 occurs?
No. The dyno keeps processing it until it finishes or Gunicorn's timeout kills the worker.
Should I lower Gunicorn's timeout on Heroku?
Yes, to around 25 seconds, so stuck workers are replaced before more requests queue behind them.
How do I run long tasks on Heroku?
Use a worker dyno with Celery or RQ and a Redis add-on, and return immediately from the web request.