cloudflare/cloudflared · warning
failed to start tcp flow due to rate limiting
Error message
failed to start tcp flow due to rate limiting
What it means
ProxyTCP guards the total number of concurrent TCP flows with p.flowLimiter.Acquire(management.TCP.String()). When too many flows are already active, Acquire returns a rate-limit error which is wrapped with this message and the TCP proxy request is rejected instead of started.
Source
Thrown at proxy/proxy.go:157
return fmt.Errorf("unrecognized service: %s, %t", rule.Service, originProxy)
}
}
// ProxyTCP proxies to a TCP connection between the origin service and cloudflared.
func (p *Proxy) ProxyTCP(
ctx context.Context,
conn connection.ReadWriteAcker,
req *connection.TCPRequest,
) error {
incrementTCPRequests()
defer decrementTCPConcurrentRequests()
logger := newTCPLogger(p.log, req)
// Try to start a new flow
if err := p.flowLimiter.Acquire(management.TCP.String()); err != nil {
logger.Warn().Msg("Too many concurrent flows being handled, rejecting tcp proxy")
return errors.Wrap(err, "failed to start tcp flow due to rate limiting")
}
defer p.flowLimiter.Release()
serveCtx, cancel := context.WithCancel(ctx)
defer cancel()
tracedCtx := tracing.NewTracedContext(serveCtx, req.CfTraceID, &logger)
logger.Debug().Msg("tcp proxy stream started")
// Parse the destination into a netip.AddrPort
dest, err := netip.ParseAddrPort(req.Dest)
if err != nil {
logRequestError(&logger, err)
return err
}
if err := p.proxyTCPStream(tracedCtx, conn, dest, p.originDialer, &logger); err != nil {
logRequestError(&logger, err)View on GitHub (pinned to 2253eeeb25)
Solutions
- Wait and retry — the limit is on concurrent flows; closing idle sessions frees capacity.
- Find and close leaked/hung TCP sessions (check active connection lists on client and server).
- Increase the flow/rate limit configuration if your workload legitimately needs more concurrent flows.
- Monitor with metrics to confirm whether flows are leaking rather than just high volume.
Example fix
// before (client)
for _, host := range hosts { go connect(host) } // bursts past flow limit
// after
sem := make(chan struct{}, 10)
for _, host := range hosts {
sem <- struct{}{}
go func(h string) { defer func() { <-sem }(); connect(h) }(host)
} Defensive patterns
Strategy: retry
Validate before calling
// Client-side: back off when the limiter is saturated
if strings.Contains(err.Error(), "rate limiting") {
select {
case <-time.After(time.Duration(rand.Intn(1000)) * time.Millisecond):
case <-ctx.Done():
return ctx.Err()
}
return retryConnect(ctx)
} Try / catch
err := p.flowLimiter.Acquire(management.TCP.String())
if err != nil {
logger.Warn().Err(err).Msg("tcp flow rejected; retry with backoff")
return errors.Wrap(err, "failed to start tcp flow due to rate limiting")
}
defer p.flowLimiter.Release() Prevention
- Bound client-side concurrency so you don't exhaust the remote flow limiter.
- Always release acquired flows (defer Release) to avoid leaks.
- Apply exponential backoff with jitter on rejection.
- Raise the flow limit if legitimate workload requires it.
When it happens
Trigger: A TCP proxying request (e.g. ssh over cloudflared access tcp, or bastion mode) arrives while the flow limiter is saturated — the number of concurrent TCP flows has hit the configured cap, so Acquire fails.
Common situations: Many simultaneous SSH-over-tunnel sessions, leaked/unclosed TCP connections accumulating over time, or a deliberately low flow limit in a constrained deployment.
Related errors
- internal error: unsupported connection type
- flow registration rate limited
- Failed to proxy HTTP: %w
- cloudflared received a warp-routing request with an empty ho
- unable to dial tcp to origin %s: %w
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/d88cf93caebe55df.
Report an issue: GitHub.