tailscale/tailscale · warning
proxy is closed
Error message
proxy is closed
What it means
Each serve/funnel proxy path owns a reverseProxy. When a serve config update tears one down, close() sets an atomic closed flag and closes idle backend connections. A request that arrives after that gets 503 'proxy is closed' instead of being forwarded; the log notes a request for a proxy that is closing or closed.
Source
Thrown at ipn/ipnlocal/serve.go:956
}
// close ensures that any open backend connections get closed.
func (rp *reverseProxy) close() {
rp.closed.Store(true)
if h2cT := rp.h2cTransport.Get(func() *http.Transport { return nil }); h2cT != nil {
h2cT.CloseIdleConnections()
}
if httpTransport := rp.httpTransport.Get(func() *http.Transport {
return nil
}); httpTransport != nil {
httpTransport.CloseIdleConnections()
}
}
func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if closed := rp.closed.Load(); closed {
rp.logf("received a request for a proxy that's being closed or has been closed")
http.Error(w, "proxy is closed", http.StatusServiceUnavailable)
return
}
p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) {
oldOutPath := r.Out.URL.Path
r.SetURL(rp.url)
// If mount point matches the request path exactly, the outbound
// request URL was set to empty string in serveWebHandler which
// would have resulted in the outbound path set to <proxy path>
// + '/' in SetURL. In that case, if the proxy path was set, we
// want to send the request to the <proxy path> (without the
// '/') .
if oldOutPath == "" && rp.url.Path != "" {
r.Out.URL.Path = rp.url.Path
r.Out.URL.RawPath = rp.url.RawPath
}
// For Unix sockets, use the URL's host (localhost) instead of the incoming hostView on GitHub (pinned to 6e0912f979)
Solutions
- Retry the request after the config change settles (seconds).
- Verify the expected proxy still exists with 'tailscale serve status'.
- Stage config changes so old and new proxies overlap instead of hard-swapping.
- If 503s persist without config churn, collect logs and report a bug.
Defensive patterns
Strategy: retry
Validate before calling
# Confirm the mount is still live before sending production traffic: $ tailscale serve status $ curl -fsS https://<machine>.<tailnet>.ts.net/<mount>/ -o /dev/null && echo ok
Try / catch
Treat HTTP 503 with 'proxy is closed' as transient: retry with backoff (for example 100ms, 500ms, 2s) plus jitter; give up once the config-change window has passed.
Prevention
- Drain traffic before removing serve mounts.
- Prefer additive config updates: add the new path first, remove the old one later.
- Watch 503 rates as a canary during reconfiguration windows.
When it happens
Trigger: An in-flight HTTP request races a serve config change that removes or replaces the proxy; tailscaled is shutting down while funnel traffic still arrives; rapid 'tailscale serve' reconfiguration under load.
Common situations: CI/CD reapplying serve config on every deploy; k8s operator updating Ingress/Service resources; scale-down or restart of the node behind a funnel front end that keeps forwarding.
Related errors
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/07969d487070b991.
Report an issue: GitHub.