#13913·traefik

p2c: inflight counters reset on configuration reload, degrading to random selection for long-lived connections

Author: wiswesserCreated Sep 15, 2026Updated Sep 17, 2026
Labelskind/enhancementpriority/P2breaking

Welcome!

  • Yes, I've searched similar issues on GitHub and didn't find any.
  • Yes, I've searched similar issues on the Traefik community forum and didn't find any.

What did you do?

Used strategy: p2c for a service whose backends hold long-lived connections (WebSockets, multi-day lifetimes), then triggered a dynamic configuration reload while those connections were still open.

Minimal reproduction with the file provider (no Kubernetes needed):

  1. Start three backends that accept a request and never return from the handler, simulating a long-lived connection.
  2. Run Traefik with the static and dynamic configuration shown below (watch: true on the file provider).
  3. Open a number of connections and leave them open. They are distributed by p2c, and inflight grows per backend as expected.
  4. Append an unrelated router to dynamic.yml to trigger a reload. Do not touch the app service or its servers, and do not close any connection.
  5. Open a second batch of connections.

Expected: the second batch is placed according to the connections still open from the first batch, i.e. concentrated on the least-loaded backends.

What did you see instead?

After the reload, every backend's inflight counter is back to 0 even though all connections from step 3 are still established. The second batch is distributed uniformly at random across all backends, ignoring the existing skew entirely.

The cause is in pkg/server/service/loadbalancer/p2c/p2c.go. The counter lives on the handler, which lives on the Balancer:

type namedHandler struct {
	http.Handler
	name     string
	inflight atomic.Int64
}

func (h *namedHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	h.inflight.Add(1)
	defer h.inflight.Add(-1)
	h.Handler.ServeHTTP(rw, req)
}

A dynamic configuration update builds a new Balancer via p2c.New() with fresh namedHandler instances, all starting at inflight == 0. Connections opened before the reload are still blocked inside the previous handler's ServeHTTP, so they keep incrementing the old, now-unreachable counters. The new balancer has no knowledge of them.

Selection then ties on zero for every pair, and the strict comparison in nextServer() falls through to h1:

// Ensure h1 has fewer inflight requests than h2.
if h2.inflight.Load() < h1.inflight.Load() {
	return h2, nil
}
return h1, nil

Since h1 = healthy[b.rand.Intn(len(healthy))], the result is a uniform random pick. p2c silently degrades to random selection until enough new connections accumulate to break the ties — which, for connections measured in days, effectively means never.

Why this matters in practice

With the Kubernetes providers, any endpoint change reloads the configuration. A pod restart or a deploy therefore does two things at once: it creates connection skew (the evicted pod's clients reconnect) and it erases the state p2c needs to correct that skew. The reconnect wave is scattered uniformly instead of being steered onto the empty pods.

What version of Traefik are you using?

v3.7.13, and reproduced on current master (built locally).

What is your environment & configuration?

Reproduction (file provider, no Kubernetes):

Static:

log:
  level: DEBUG
entryPoints:
  web:
    address: ":8000"
providers:
  file:
    filename: dynamic.yml
    watch: true

Dynamic:

http:
  routers:
    app:
      rule: PathPrefix(`/`)
      service: app
  services:
    app:
      loadBalancer:
        strategy: p2c
        servers:
          - url: "http://127.0.0.1:9001"
          - url: "http://127.0.0.1:9002"
          - url: "http://127.0.0.1:9003"

Originally observed with the kubernetesCRD provider: IngressRoute with strategy: p2c, nativeLB: false, 3 backend pods, Traefik scaled to 1 replica to rule out per-instance counter splitting.

If applicable, please paste the log output in DEBUG level

No response