Graceful Shutdown¶
This page describes what LeanProxy-MCP does when it stops: which requests still get an answer, how upstream servers are stopped, and what is cleaned up. The behaviour differs by front end.
Summary¶
server run --stdio |
server run --http |
serve (deprecated) |
|
|---|---|---|---|
| Stopped by | SIGINT/SIGTERM, stdin EOF, a shutdown request |
SIGINT/SIGTERM |
SIGINT/SIGTERM |
| In-flight client requests | Signal: dropped. EOF or shutdown: up to 5 s to finish, then canceled |
Canceled at once; the HTTP server waits up to 5 s for handlers to return | Dropped |
| Upstream stdio servers | SIGTERM to the process group, SIGKILL after 5 s |
same | same |
| Spilled results, status file | Removed | Removed | Removed |
| Exit status | 0 (1 if reading stdin failed) |
0 |
0 |
Stdio front end (server run --stdio)¶
This is the mode an IDE runs. It stops in three ways.
The client closes stdin, or sends shutdown¶
This is the normal end of a session.
- The proxy stops reading stdin.
- Requests still running get up to 5 seconds to finish. Their responses are written as usual.
- Requests still running after 5 seconds are canceled. The proxy waits at most 2 more seconds for them to return.
- For a
shutdownrequest, the proxy now answers it with{"status": "shutdown"}. The answer comes last, so the client knows every earlier request is done. - The upstream servers are stopped (see Stopping upstream servers), the usage data is flushed, and the status file is removed.
- The process exits with status
0.
shutdown is LeanProxy's own request, not part of the MCP specification.
Clients that never send it are fine: closing stdin has the same effect.
SIGINT or SIGTERM¶
A signal does not wait for running requests. The proxy:
- flushes the usage data and removes the status file;
- stops the upstream servers;
- deletes spilled results and flushes telemetry (at most 5 seconds);
- exits with status
0.
Requests that were running get no response.
Streamable HTTP front end (server run --http)¶
The HTTP front end stops only on SIGINT or SIGTERM:
- The usage data is flushed.
- New sessions are refused. Every open session is ended: its running requests are canceled and its streams are closed.
- The HTTP server stops accepting connections and waits up to 5 seconds for handlers to return, then closes what is left.
- The status file is removed, the upstream servers are stopped, spilled results are deleted and telemetry is flushed.
- The process exits with status
0.
A client that sends shutdown over HTTP gets {"status": "shutdown"}, but
the proxy keeps running for its other clients. To end one session, the
client sends DELETE with its Mcp-Session-Id. Idle sessions end on their
own after server.http.session_idle_timeout (default 30m).
Deprecated line-TCP front end (serve)¶
On the first SIGINT or SIGTERM, serve:
- flushes the usage data and stops the registry refresh and the health checker;
- closes the dashboard and
/metricslisteners and removes the status file; - stops the sidecar and closes the TCP listener;
- stops the semantic cache, deletes spilled results and stops the upstream servers;
- closes the vector store and flushes telemetry (at most 5 seconds);
- exits with status
0.
Open client connections are not drained. Later signals are ignored while it
shuts down. SIGHUP does not stop serve: it re-reads the
--providers-config file and rebuilds the redactor. serve does not stop
on a shutdown request.
Stopping upstream servers¶
All front ends stop upstream servers the same way, in parallel:
- Requests waiting on a stdio server fail at once with
pool: server <name> is stopping, instead of waiting for their timeout. - stdio servers run in their own process group (Unix). The proxy sends
SIGTERMto the whole group, so processes started bynpx,uvx,docker runorsh -cstop too. A server that is still running after 5 seconds getsSIGKILL. Any process left in its group at that point is killed as well. - Sandboxed servers: the container is removed with
docker rm -f(orpodman rm -f), bounded to 5 seconds. - HTTP and SSE servers: their connections are closed.
- The proxy waits at most 10 seconds in total for all stdio servers to
stop, then logs
timed out waiting for stdio servers to stopand goes on.
On Windows, a server is killed at once; there is no process group or
SIGTERM.
The same stop sequence runs when a single server is stopped or restarted
while the proxy keeps running: after its idle_timeout, or after failed
health checks.
What is cleaned up¶
| Item | Location | On shutdown |
|---|---|---|
| Status file | ~/.config/leanproxy/status/current.json |
Removed. status --running then reports no running proxy |
Spilled tool results (response.spill) |
Memory, or ~/.leanproxy/results/<pid>-<id>/ with spill.disk: true |
Deleted. If the process was killed, the directory is deleted by the next start once it is older than spill.ttl |
Usage data for report |
~/.leanproxy/usage/ |
Flushed to disk, kept |
| Tool cache, pins, quarantine | ~/.config/leanproxy/, ~/.leanproxy/quarantine/ |
Kept |
| Telemetry | OTLP collector | Pending spans and metrics are sent, at most 5 seconds |
SIGKILL on the proxy itself skips all of this. Its child servers get no
signal: most exit when their stdin closes. The status file stays until the
next start replaces it.
For contributors¶
The shutdown order lives in cmd/:
cmd/server.go(runServerRun) builds aclosePoolsfunction that stops the background refresh and the health checker, then callsStdioPool.Close(),HTTPClientPool.Close(),SSEPool.Close(),Governor.Close()and the telemetry provider'sShutdown. It runs once, from the signal handler or after the front end returns.cmd/stdio_frontend.go(serveStdio) implements the drain:defaultStdioShutdownGrace(5 s), then cancel, thenstdioCancelWait(2 s). The handler's ownshutdownmethod (pkg/mcp) only acknowledges; the front end owns the order.cmd/http_frontend.go(handleHTTP) callsstreamhttp.Server.ShutdownwithhttpShutdownGrace(5 s).cmd/serve.gohas its own signal handler, which ends inos.Exit(0).pkg/pool:StdioPool.Close()stops every server in parallel withdefaultStopGracePeriod(5 s) and waits at most that pluscloseDeadlineMargin(5 s). Rate limiters hold no goroutine and need no shutdown.pkg/mcp/governor:Store.Close()deletes the spill store, andpruneStaleDirsremoves stale per-process directories at start.
Next Steps¶
- Configuration - Every key of
leanproxy_servers.yaml - Architecture - System architecture