Fix Gunicorn "Error Handling Request" and "Ignoring EPIPE" With Django
Understand and fix Gunicorn log lines such as Error handling request, Ignoring EPIPE, and BrokenPipeError with Django: tell client disconnects apart from real errors, align proxy and Gunicorn timeouts, and handle slow uploads and streaming responses.
Problem statement
Gunicorn's error log has lines like these, sometimes dozens per hour:
[ERROR] Error handling request /api/export/
Traceback (most recent call last):
...
BrokenPipeError: [Errno 32] Broken pipe
[DEBUG] Ignoring EPIPE
[ERROR] Error handling request (no URI read)
They look alarming, but they usually mean the client went away before Gunicorn finished sending the response: a user closed the tab, a mobile connection dropped, a proxy gave up waiting, or a load balancer health check disconnected early. When Gunicorn tries to write to a socket the other side has closed, the operating system returns EPIPE. The question is whether the disconnects are normal background noise or a symptom of timeouts that cut off slow responses.
- Correlate timestamps with the proxy logs: Nginx logs
499when the client closed the connection. - Occasional EPIPE on slow pages is normal; frequent ones on one endpoint mean it is too slow.
- Make the proxy and load balancer time out after Gunicorn, not before: proxy timeout > Gunicorn
timeout. (no URI read)errors usually come from health checks or scanners opening and closing connections; they can be ignored.- Speed up or offload slow endpoints (exports, reports) to Celery instead of raising every timeout.
Step-by-step solution
1. Match the error with the proxy log
grep "Error handling request /api/export/" /var/log/gunicorn/error.log | tail -3
grep '/api/export/' /var/log/nginx/access.log | awk '$9==499' | tail -3
Nginx status 499 at the same time confirms the client closed the connection. If Nginx logs 504 instead, Nginx itself timed out waiting for Gunicorn and closed the upstream connection, which Gunicorn then reports as a broken pipe.
2. Order the timeouts
Each hop should wait longer than the one behind it, so the app server times out first and returns a clear error:
client / CDN > load balancer > Nginx proxy_read_timeout > Gunicorn timeout
# gunicorn.conf.py
timeout = 30
graceful_timeout = 30
keepalive = 5
location / {
proxy_pass http://app;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
An AWS ALB idle timeout of 60 seconds with proxy_read_timeout 120s and Gunicorn timeout = 120 means the ALB disconnects first and every slow request becomes a broken pipe.
3. Ignore noise from health checks and scanners
Error handling request (no URI read) means a connection was opened and closed before a request line arrived. TCP health checks, port scanners, and some load balancers do this. Configure HTTP health checks rather than TCP checks where possible, and do not treat these lines as incidents.
4. Fix slow endpoints
If the broken pipes concentrate on one endpoint, it is too slow for its clients. Options:
- Stream large CSV exports with
StreamingHttpResponseso data starts flowing immediately. - Move reports and exports to a Celery task and email or notify the user with a download link.
- Add pagination or database indexes to slow list views.
5. Handle slow uploads
Sync workers read the request body while the client uploads it. Slow clients tie up workers and can trip timeouts in the middle of an upload. Nginx buffers request bodies by default (proxy_request_buffering on), which protects Gunicorn; keep it on. For direct-to-Gunicorn setups without Nginx, use gthread or async workers.
Explanation
A TCP connection can be closed by either side at any time. When the client side closes and Gunicorn later calls send(), the kernel returns EPIPE (or the connection reset variant ECONNRESET). Python raises BrokenPipeError, and Gunicorn logs the traceback under "Error handling request", or quietly with "Ignoring EPIPE" at debug level. Nothing is wrong with the worker; it simply had nobody left to talk to. The work Django did for that request is wasted, which is why frequent broken pipes on expensive endpoints are worth fixing.
Edge cases / notes
- Transactions. A client disconnect does not roll back a view that already committed; design actions like payments to be idempotent so retries are safe.
- Streaming. Long streaming responses and server-sent events see broken pipes whenever users navigate away. That is expected.
- Log levels. Gunicorn at
loglevel = debugprints every "Ignoring EPIPE"; production usually runs atinfo.
Next steps
- See the proxy side in Fix Nginx 499 Client Closed Request.
- If workers are killed instead, read Fix Gunicorn Worker Timeout in Django.
- Tune settings in Configure gunicorn.conf.py for Django.
FAQ
Is "Error handling request" a bug in my code?
Usually not. If the traceback ends in BrokenPipeError or ConnectionResetError, the client disconnected.
Should I raise Gunicorn's timeout to stop it?
Only if the proxy and load balancer timeouts are raised to match and the slow endpoint cannot be made faster. Faster responses are the better fix.
What does "no URI read" mean?
A connection closed before sending a request line, typically a TCP health check or a scanner.
Can broken pipes cause data loss?
The response is lost, but the work in the view still ran. Make important actions idempotent so retries are safe.