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 host

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Retry the request after the config change settles (seconds).
  2. Verify the expected proxy still exists with 'tailscale serve status'.
  3. Stage config changes so old and new proxies overlap instead of hard-swapping.
  4. 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

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.