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

  1. Wait and retry — the limit is on concurrent flows; closing idle sessions frees capacity.
  2. Find and close leaked/hung TCP sessions (check active connection lists on client and server).
  3. Increase the flow/rate limit configuration if your workload legitimately needs more concurrent flows.
  4. 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

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


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/d88cf93caebe55df. Report an issue: GitHub.