Skip to main content
← Back to BlogEngineering

The user closed the tab. The query didn't get the memo.

A browser tab closes mid-request and the backend never finds out — a tool call keeps running, a proxy keeps streaming, a warehouse connection stays open for a client that's gone. Here's why disconnect has to propagate as a real cancellation signal through every hop, not just get swallowed at the edge.

DA
DataAgents Team|Product & Data·July 29, 2026·6 min read

A user opens a chat, asks a question that needs a warehouse query, and closes the tab three seconds later because they got pulled into a meeting. Nothing about that is unusual — it happens dozens of times a day. The question is what happens to the work that was already in flight.

The failure mode nobody notices

In a synchronous request/response world, a closed connection kills the request. In an agentic chat system, it doesn't, by default. The browser tab is gone, but the server-side turn is still running: a tool call is mid-flight against the warehouse, an LLM completion is still streaming tokens nobody will read, and a downstream API proxy is still holding an open connection to a backend that thinks a client is waiting.

None of this throws an error. There's no client to report the error to. The work just keeps running until it finishes on its own — burning compute, holding a database connection, and, if the tool call was a warehouse query, potentially finishing minutes after anyone could act on the result.

We hit this in two different layers of the same system, and the fix in both cases was the same idea: treat client disconnect as a first-class signal, not an edge case you find out about later.

Layer one: tool calls that never resolve

The agent runtime tracks every in-flight tool call in a map keyed by tool call ID — warehouse queries, schema lookups, whatever the agent decided to run mid-turn. Under normal conditions each entry resolves when the tool finishes and the runtime streams the result back over SSE.

The problem is what a client disconnect, an idle stream, or an abort leaves behind. Before this was handled explicitly, a dropped connection left those tool calls dangling: the UI's last known state was “Querying…” forever, and reloading the page didn't help, because the turn had never been persisted as finished or failed — it was just stuck.

The fix is a settlement pass. The moment the runtime detects the stream is idling, aborting, or the client is gone, it walks the pending tool call map and force-emits a terminal event — a tool-output-error — for every call still open, then persists the turn in that terminal state before closing anything. A reload now shows an honest “this didn't finish” instead of a UI stuck mid-sentence.

Layer two: the proxy that outlives the browser tab

One layer down, the API proxy sitting between the frontend and the backend had the mirror-image problem. A fetch to the backend is a stream. If the browser tab closes while that stream is still flowing, the proxy's request to the backend doesn't know that. Without explicit propagation, the backend keeps generating a response — tokens, warehouse rows, whatever — for a client that will never read a single byte of it.

The fix here is narrower but just as important: the proxy listens for the incoming client request to close, and on that event fires an AbortController signal into the upstream fetch. The backend gets a real cancellation, not a silent orphan, and the resources it was holding — a connection, a query cursor, a model context — get released immediately instead of whenever the response happens to finish.

Why this is a cost problem, not just a hygiene problem

It's tempting to file “clean up after disconnected clients” under nice-to-have. In an agentic system it isn't. Every tool call that keeps running after its client is gone is billed compute with no possible payoff — an LLM still generating tokens, a warehouse still executing a query, a connection pool slot held by nothing. At low volume this is noise. At the volume an autonomous data agent runs tool calls — often several per turn, across many concurrent users — orphaned work becomes a real line item, and it's one that's invisible in normal monitoring because nothing errors and nothing pages anyone.

It's also a correctness problem in disguise. A tool call that finishes after the client has moved on can still write its result into shared state — a cache, a conversation history, a persisted turn — arriving late and stale, confusing whichever process reads it next.

The pattern: cancellation has to propagate every hop

The general shape of the fix is the same at both layers, and it generalizes past these two specific systems:

text
client disconnects
  -> edge/proxy detects the close event
  -> AbortController.abort() fires on the upstream call
  -> runtime sees the abort, not just a timeout
  -> in-flight tool calls are force-settled to a terminal state
  -> the turn is persisted as “interrupted”, not left open
  -> resources (connections, cursors, model contexts) are released now

Each hop has to actively pass the signal on — a proxy that swallows the disconnect and keeps its own upstream call open defeats the whole chain, no matter how well the layer behind it handles cancellation. And each layer needs a terminal state for “this got interrupted,” distinct from both “succeeded” and “failed with an error,” so a reload or a retry has an honest starting point instead of an unresolved gap.

None of this is exotic engineering. It's the unglamorous work of making sure every promise you make when you start a tool call — “I will eventually resolve” — has a defined answer even when the thing that asked for it disappears. For an agent system running unattended, at scale, on someone else's compute budget, that defined answer is the difference between a system that degrades gracefully and one that quietly leaks money and state until someone notices the bill.

See it in action

Connect your data sources and get your first automated report in under a week.

Book a Demo →
Ready when you are

See Your Data Clearly - Without Building a Data Team.

Connect your sources, standardize your metrics, and get decision-ready answers in minutes.

We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.