Troubleshooting
Gunicorn & app servers

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.

Why the pipe breaksDjango worksslow responseClient or proxygives up firstGunicorn writesto closed socketEPIPE loggedbroken pipeThe client stopped listening before Gunicorn finished writing the response.
  1. Correlate timestamps with the proxy logs: Nginx logs 499 when the client closed the connection.
  2. Occasional EPIPE on slow pages is normal; frequent ones on one endpoint mean it is too slow.
  3. Make the proxy and load balancer time out after Gunicorn, not before: proxy timeout > Gunicorn timeout.
  4. (no URI read) errors usually come from health checks or scanners opening and closing connections; they can be ignored.
  5. 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.

each hop waits longer than the next (seconds)Gunicorn timeouttimes out firstNginx proxy_read_timeout45 sLoad balancer idle timeout60 sCDN origin timeout70 s0 s30 s60 sInner layers should give up first so the error is clean and loggeddjango-deployment.com
When an outer layer gives up first, Gunicorn sees a broken pipe instead.

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 StreamingHttpResponse so 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.

read the message and the proxy logPatternMeaning and actionEPIPE with Nginx 499client left; fine ifrare, else speed upEPIPE with Nginx 504proxy timed out first;reorder timeoutsno URI readhealth check orscanner; safe to ignoremany on one endpointtoo slow; stream or move to CeleryBroken pipes are a symptom; the proxy log shows the causedjango-deployment.com
Count broken pipes per endpoint before deciding they matter.

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 = debug prints every "Ignoring EPIPE"; production usually runs at info.

Next steps

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.

Keep going