tailscale/tailscale · warning
cannot accept connection; rate limited
Error message
cannot accept connection; rate limited
What it means
derper wraps its listener with a token-bucket rate limiter (--accept-connection-limit rate with --accept-connection-burst burst; both effectively unlimited by default, derper.go:88-89). When the bucket is empty, Accept() returns errLimitedConn and bumps counter_rejected_connections; the comment notes connections are still accepted and immediately closed so clients see an active server and retry with backoff rather than piling up in the kernel backlog.
Source
Thrown at cmd/derper/derper.go:511
numRejects expvar.Int
net.Listener
lim *rate.Limiter
}
func newRateLimitedListener(ln net.Listener, limit rate.Limit, burst int) *rateLimitedListener {
return &rateLimitedListener{Listener: ln, lim: rate.NewLimiter(limit, burst)}
}
func (ln *rateLimitedListener) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_accepted_connections", &ln.numAccepts)
m.Set("counter_rejected_connections", &ln.numRejects)
return m
}
var errLimitedConn = errors.New("cannot accept connection; rate limited")
func (ln *rateLimitedListener) Accept() (net.Conn, error) {
// Even under a rate limited situation, we accept the connection immediately
// and close it, rather than being slow at accepting new connections.
// This provides two benefits: 1) it signals to the client that something
// is going on on the server, and 2) it prevents new connections from
// piling up and occupying resources in the OS kernel.
// The client will retry as needing (with backoffs in place).
cn, err := ln.Listener.Accept()
if err != nil {
return nil, err
}
if !ln.lim.Allow() {
ln.numRejects.Add(1)
cn.Close()
return nil, errLimitedConn
}
ln.numAccepts.Add(1)View on GitHub (pinned to cfe32b8be6)
Solutions
- Treat it as expected behavior first — Tailscale clients retry with backoff, so brief bursts are harmless
- If legitimate traffic is being shed, raise --accept-connection-limit and --accept-connection-burst to fit your client population
- Check /debug/vars counter_rejected_connections over time to size the limit instead of guessing
- If caused by health-check probes, spread or slow the probes rather than raising the global limit
Example fix
# before derper -a :443 --accept-connection-limit=5 # after (sized for reconnect storms) derper -a :443 --accept-connection-limit=100 --accept-connection-burst=200
Defensive patterns
Strategy: retry
Validate before calling
curl -s "http://derper-host/debug/vars" | python3 -c "import json,sys; d=json.load(sys.stdin); r=d.get('counter_rejected_connections',{}); print('rejected:', r)" Try / catch
// Client-side accept loop pattern when embedding derper's listener type:
for {
conn, err := ln.Accept()
if err != nil {
if err.Error() == "cannot accept connection; rate limited" || errors.Is(err, errLimitedConn) {
time.Sleep(backoff.Next()) // server closed conn intentionally; retry with backoff
continue
}
return err // real listener error
}
go handle(conn)
} Prevention
- Monitor counter_rejected_connections vs counter_accepted_connections on /debug/vars to size limits from data
- Account for reconnect storms when sizing --accept-connection-limit (all clients reconnect after a derper restart)
- Keep burst >= expected simultaneous reconnect wave to shed only true floods
When it happens
Trigger: derper started with a finite --accept-connection-limit (or -connection-rate-limit style config) and the incoming connection rate exceeding the rate+burst budget — e.g. after a derper restart when all clients reconnect at once, an aggressive health checker, or a connection flood.
Common situations: Operators who set a low accept rate for protection then see reconnect storms (mass client reconnect, LB health probes from many sources) hit the limit; visible via the derper expvar metrics counter_rejected_connections on /debug/vars.
Related errors
- only ports 443 and 80 are allowed
- register: %w
- waiting for order: %w
- finalizing order: %w
- getting authorization: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/cc71af32584f385d.
Report an issue: GitHub.