Sticky sessions in Traefik bind a client to one backend by setting a cookie that names the chosen server. Subsequent requests carry the cookie, Traefik reads it, and pins traffic. The cookie is short and opaque — it does not leak server hostnames externally. Sticky sessions are correct for in-memory sessions or local per-instance caches; they are wrong for stateless backends and they have failure modes when the pinned backend disappears. This article covers the config, the security attributes that matter, and the gotchas.
How to verify
# A fresh request with no sticky cookie lands somewhere; Traefik sets the cookie
curl -sI -c /tmp/cookies.txt https://app.example.com/ | grep -i set-cookie
# The cookie should be Set-Cookie: <name>=<short-id>; Path=/; Secure; HttpOnly; SameSite=Lax
# Subsequent requests with the cookie hit the same backend
curl -sI -b /tmp/cookies.txt https://app.example.com/ -o /dev/null -w "%{http_code} %{time_connect}\n"
curl -sI -b /tmp/cookies.txt https://app.example.com/ -o /dev/null -w "%{http_code} %{time_connect}\n"
# The backend access log should show the same upstream IP for both
docker logs traefik 2>&1 | grep app.example.com | tail -3
What’s happening
Traefik’s sticky session implementation is straightforward: enable it on a service, give the cookie a name, and Traefik picks a server on the first request, sets a cookie that encodes a short opaque server ID, and subsequent requests carrying that cookie are pinned to the same backend.
The cookie value is not the backend’s hostname — Traefik uses an obfuscated identifier so internal topology doesn’t leak. The cookie attributes (Secure, HttpOnly, SameSite) are configurable; defaults are sensible for HTTPS sites and should be tightened explicitly for anything sensitive.
The failure mode that surprises operators: when the pinned backend disappears (deploy, autoscale-down, crash), Traefik either falls back to a different backend silently (the request succeeds but the user’s in-memory session is gone) or returns a 502 (if you’ve configured strict stickiness). Stateless backends that don’t care which replica handles the request should not use sticky sessions — every pin reduces load-balancing efficiency.
Sticky sessions are useful for: server-side sessions stored in memory; WebSocket connections that benefit from connection reuse; admin UIs where switching servers loses CSRF tokens; specific framework patterns (PHP file-based sessions, Java HttpSession) where moving sessions across servers is expensive.
The procedure
-
Enable stickiness on a service in file provider config.
http: services: app-backend: loadBalancer: sticky: cookie: name: srv_id secure: true httpOnly: true sameSite: lax servers: - url: "http://10.0.1.10:8080" - url: "http://10.0.1.11:8080" - url: "http://10.0.1.12:8080" healthCheck: path: /healthz interval: 5s -
Apply with Docker labels on a container.
docker run -d \ --label traefik.enable=true \ --label "traefik.http.routers.app.rule=Host(\`app.example.com\`)" \ --label traefik.http.services.app.loadbalancer.sticky.cookie.name=srv_id \ --label traefik.http.services.app.loadbalancer.sticky.cookie.secure=true \ --label traefik.http.services.app.loadbalancer.sticky.cookie.httponly=true \ --label traefik.http.services.app.loadbalancer.sticky.cookie.samesite=lax \ myapp/server -
Kubernetes IngressRoute equivalent — sticky is set on the Service definition in the cluster via the chart’s options, or via a TraefikService.
apiVersion: traefik.io/v1alpha1 kind: TraefikService metadata: name: app-sticky namespace: my-app spec: weighted: services: - name: app port: 8080 weight: 100 sticky: cookie: name: srv_id secure: true httpOnly: true sameSite: lax --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: app namespace: my-app spec: entryPoints: [websecure] routes: - match: Host(`app.example.com`) kind: Rule services: - name: app-sticky kind: TraefikService -
For multi-cookie patterns (your app sets its own session cookie, Traefik adds the sticky cookie), name the sticky cookie clearly so a casual reader of the browser cookie jar can tell them apart.
srv_idis short and ignorable;JSESSIONID(a common Java framework cookie) is reserved — don’t reuse it. -
Confirm stickiness with health checks present — without active health checking, a dead backend keeps receiving pinned traffic from cookies until the cookie expires or the user opens a new session.
http: services: app-backend: loadBalancer: sticky: cookie: name: srv_id servers: - url: "http://10.0.1.10:8080" - url: "http://10.0.1.11:8080" healthCheck: path: /healthz interval: 5s timeout: 2s -
Audit cookie attributes. The cookie must be
Secureon production HTTPS,HttpOnlyalways, andSameSite=LaxorStrictdepending on whether your app expects cross-site navigation.curl -sI https://app.example.com/ | grep -i 'set-cookie.*srv_id'
Operational notes
- Stickiness reduces load-balancing efficiency — a 10-replica fleet with sticky sessions can have 1 replica at 80% CPU and 9 idle.
- The sticky cookie is set on the first request; clients that strip cookies (curl scripts, some monitoring) will land on different backends each time, which can mask intermittent issues.
- When you autoscale down, the user pinned to the departing replica gets reassigned — if they had in-memory state, it’s gone. For real session continuity, push sessions to a shared store (Redis, database) instead of relying on stickiness.
- Behind a CDN that caches, the sticky cookie can be cached too — a multi-user device hits the same backend forever. Vary the cache on the cookie or exclude it from cache.
- For WebSocket-only stickiness, the cookie is set on the HTTP upgrade response; thereafter the connection itself is pinned and stickiness becomes moot until the next connection.
Sticky sessions are a workaround for stateful backends, not a load-balancing feature. The right long-term answer for most apps is moving the state out of the replica so any backend can serve any request — but where state can’t move, stickiness done correctly is invisible to users. The audit of which services rely on it (and the plan to remove the dependency) is part of managed operations. For traffic-splitting patterns that don’t pin see traefik-blue-green-canary.