Can a Flask view detect that the client disconnected and stop long-running work early?
0 reputation · 23 Apr 2020, 12:54 UTC
0 reputation · 23 Apr 2020, 12:54 UTC
I have a Flask application with a few synchronous view functions that run expensive operations — report generation and large data exports that can take 30–60 seconds. If a user closes the browser tab or cancels the request halfway through, I would like the view to stop working and release its resources instead of running to completion for a response nobody will receive.
My understanding is that Flask itself provides no request-level cancellation signal, and that under a WSGI server like Gunicorn the worker thread keeps executing the view even after the client disconnects. I have seen references to request.is_disconnected and to asyncio.CancelledError in async views, but it is unclear which of these are portable across servers and which depend on specific ASGI behavior like the http.disconnect event.
Assume Flask 2.x/3.x deployed behind Gunicorn (sync workers), with the option to move to an ASGI server such as Hypercorn if that changes the answer.
Is there any documented, server-independent way for a Flask view to detect a client disconnect mid-request and abort work? If not, does converting the view to async def under an ASGI server reliably deliver cancellation, and are there caveats with blocking calls inside it? Finally, is polling request.is_disconnected between chunks of work a supported pattern, or is it server-specific?
35275 reputation · 23 Apr 2020, 17:33 UTC
No — there is no documented, server-independent way for a Flask view to receive a cancellation signal mid-request. Under WSGI (Gunicorn sync workers, your stated setup), the worker thread keeps executing the view after the client disconnects; the disconnect is usually only noticed when the server tries to write the response. request.is_disconnected is a best-effort, server-dependent check, not a cancellation primitive. True cancellation requires running as a real ASGI application, and even then it only interrupts at await points.
The WSGI contract has no channel for the server to tell your application "the client went away." Gunicorn's sync worker calls your view and waits for a return value. If the socket closes meanwhile, nothing interrupts the Python code — the failure surfaces later as a write error when the response is sent. This is protocol-level, not a Flask limitation you can configure around.
request.is_disconnected: useful, but not a contractWerkzeug's request.is_disconnected attempts a non-blocking read on the underlying socket to see if the peer closed the connection. Polling it between chunks of work is a pragmatic early-exit pattern:
from flask import request
@app.route("/report")
def report():
for chunk in generate_report_chunks():
if request.is_disconnected():
app.logger.info("client gone, aborting")
return "", 499 # never actually delivered
process(chunk)
return send_file(output)It often works with Gunicorn sync workers on direct connections, but treat it as an optimization, not a guarantee:
try/finally so temp files and DB transactions are cleaned up even when the work runs to completion for nobody.Converting to async def under standard Flask does not deliver cancellation. Flask 2.x/3.x bridges async views into the sync WSGI context, so no ASGI http.disconnect event ever reaches your code. To get real cancellation you must serve the app as a genuine ASGI application — via Hypercorn/Uvicorn with an ASGI wrapper, or by moving to Quart. Then the server can surface the disconnect and cancel the task, raising asyncio.CancelledError in the view.
Caveats, and they matter for your 30–60 second workloads:
CancelledError is only delivered at await points. CPU-bound loops, synchronous DB drivers, and time.sleep block the event loop and cannot be interrupted — the work must be awaited cooperatively or offloaded to threads/processes with explicit cancellation checks.http.disconnect on the receive channel) vary between Hypercorn, Uvicorn, and Daphne — confirm on the server you choose.For operations this long, the architecture-level answer is to get them out of the request/response cycle: enqueue a Celery/RQ job, return 202 with a job ID, and let the client poll or receive a webhook. The job checks a cancellation flag the client sets explicitly or on disconnect. This works identically on every server and survives proxies, retries, and page reloads.
request.is_disconnected and logging the result.curl, kill it mid-request (Ctrl-C), and check whether and when the flag flips under your Gunicorn setup.await asyncio.sleep(...), cancel the client, and confirm CancelledError fires and your finally cleanup runs.Behavioral details here are version- and server-sensitive; run the verification steps against your exact Werkzeug/Gunicorn/proxy combination before relying on disconnect detection in production.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.