Skip to content

Traefik sticky sessions — cookie-based affinity, secure attributes, and the limits

Enable sticky sessions in Traefik — cookie name, secure attributes, when stickiness is correct, and the failure modes when a backend disappears.

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

  1. 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
  2. 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
  3. 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
  4. 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_id is short and ignorable; JSESSIONID (a common Java framework cookie) is reserved — don’t reuse it.

  5. 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
  6. Audit cookie attributes. The cookie must be Secure on production HTTPS, HttpOnly always, and SameSite=Lax or Strict depending 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.