InfraRunBook
    Back to articles

    Traefik Sticky Sessions Not Working

    Traefik
    Published: Apr 17, 2026
    Updated: Apr 17, 2026

    Sticky sessions in Traefik fail silently and for a surprising number of distinct reasons. This runbook walks through every common root cause with real commands, log output, and concrete fixes.

    Traefik Sticky Sessions Not Working

    Symptoms

    You've configured sticky sessions in Traefik, deployed your stack, and something is still wrong. Users are randomly getting logged out. A shopping cart empties between page loads. A stateful WebSocket application keeps re-authenticating. Or you're watching your access logs and noticing the same client IP hitting different backend instances on consecutive requests — requests that should be pinned to one.

    In my experience, sticky session failures in Traefik are particularly frustrating because they fail silently. Traefik doesn't throw an error. It doesn't emit a warning. It just quietly ignores the stickiness and routes however it feels like. The first signal is almost always a user complaint or a broken feature in staging.

    The symptoms vary depending on the root cause. Sometimes no sticky cookie appears in the response at all. Sometimes the cookie is there but the client stops sending it after the first request. Sometimes the cookie exists on both sides of the connection but Traefik still ignores it and round-robins anyway. Here's what a healthy sticky session looks like — and what you should be checking for:

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: STICKY_SESSION=http%3A%2F%2F192.168.1.101%3A8080; Path=/; HttpOnly; Secure; SameSite=None

    That URL-encoded value is Traefik telling the client which backend it was assigned to. If that header is missing, garbled, or changing with every response, one of the causes below is responsible. Work through them in order — the first three are by far the most common in production environments.


    Root Cause 1: Cookie Not Being Set

    This is the most fundamental failure and the first thing to rule out. Traefik isn't sending a sticky session cookie at all, which means there's nothing for the client to return, and nothing for Traefik to route on. Without the cookie, every request is treated as a fresh session.

    Why it happens: Sticky sessions in Traefik are not enabled by default. You have to explicitly configure them on the service object — not the router. A lot of engineers configure routers correctly (TLS, entrypoints, middlewares) and never touch the service definition, assuming sticky sessions are a routing concern. They're not. They belong to the load balancer section of the service, and if that block is absent, Traefik uses its default balancing strategy and ignores any notion of session affinity.

    How to identify it: Run a request and look for the sticky cookie header:

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -iE "set-cookie|sticky"

    If you get no output, or only application-level cookies with no Traefik-managed sticky cookie, the feature isn't configured. Confirm by querying the Traefik API directly on sw-infrarunbook-01:

    curl -s http://sw-infrarunbook-01:8080/api/http/services | jq '.[] | select(.name | contains("my-app")) | .loadBalancer'

    If the output is missing a

    sticky
    block entirely, that confirms it:

    {
      "servers": [
        { "url": "http://192.168.1.101:8080" },
        { "url": "http://192.168.1.102:8080" }
      ],
      "passHostHeader": true
    }

    How to fix it: Add the sticky session block to your service definition. In a YAML file-based configuration:

    http:
      services:
        my-app:
          loadBalancer:
            sticky:
              cookie:
                name: STICKY_SESSION
                httpOnly: true
                secure: true
            servers:
              - url: "http://192.168.1.101:8080"
              - url: "http://192.168.1.102:8080"

    If you're using Docker labels, the sticky config goes on the service, not the router:

    traefik.http.services.my-app.loadbalancer.sticky.cookie.name=STICKY_SESSION
    traefik.http.services.my-app.loadbalancer.sticky.cookie.httponly=true
    traefik.http.services.my-app.loadbalancer.sticky.cookie.secure=true

    After applying the change, re-run the curl and you should see the cookie in the response headers. If you don't, keep reading.


    Root Cause 2: Wrong Service Configuration

    Traefik's object model draws a strict line between routers, middlewares, and services. It's entirely possible to configure sticky sessions on a service that no router is actually using — in which case the config exists but is never applied.

    Why it happens: In Docker environments, Traefik auto-generates service names from container names and Compose project names using a

    name@docker
    or
    name-projectname@docker
    convention. If you define sticky session labels on a service named
    my-app
    but your router is resolving to
    my-app-webstack@docker
    , the sticky config is orphaned. I've seen this trip up engineers repeatedly when container names don't match the Compose project prefix or when multiple Compose files are in play.

    How to identify it: Find out which service your router is actually pointing to:

    curl -s http://sw-infrarunbook-01:8080/api/http/routers | \
      jq '.[] | select(.name | contains("my-app")) | {name, service}'
    {
      "name": "my-app-secure@docker",
      "service": "my-app-webstack@docker"
    }

    Now check whether the sticky config exists on that exact service name:

    curl -s http://sw-infrarunbook-01:8080/api/http/services/my-app-webstack@docker | \
      jq '.loadBalancer.sticky'

    If this returns

    null
    , you've confirmed the issue. Your sticky labels reference the wrong service name and are being ignored.

    How to fix it: Use the API-confirmed canonical service name in your labels. Better yet, explicitly declare the service name in your router label so there's no ambiguity:

    traefik.http.routers.my-app.service=my-app
    traefik.http.services.my-app.loadbalancer.sticky.cookie.name=STICKY_SESSION
    traefik.http.services.my-app.loadbalancer.sticky.cookie.httponly=true

    By explicitly wiring the router to a named service, you control the name and eliminate the auto-generation guesswork entirely.


    Root Cause 3: Backend Not Receiving the Cookie

    This variant is more subtle. The sticky cookie gets set on the first response, the client stores it and sends it back correctly, but subsequent requests still land on different backends. Traefik is receiving the cookie — it's just not acting on it.

    Why it happens: Traefik stores the sticky cookie value as a URL-encoded reference to the backend server. If that backend restarts, scales down, or gets a new IP address, the cookie value no longer matches any current server in the pool. Traefik can't resolve the mapping, falls back to its load balancing strategy, and effectively treats the request as cookieless. The backend instance the client was pinned to is gone, and Traefik doesn't have a way to redirect gracefully.

    A second variant: the client isn't sending the cookie back at all. This happens when the cookie's

    Path
    attribute doesn't match the path of the subsequent requests — the browser won't include a cookie scoped to
    /api
    on a request going to
    /dashboard
    .

    How to identify it: Enable Traefik's access logs and watch which backend handles each request from the same client IP:

    tail -f /var/log/traefik/access.log | grep "192.168.50.10"
    192.168.50.10 - - [17/Apr/2026:14:22:01 +0000] "GET /dashboard HTTP/1.1" 200 4821 "-" "Mozilla/5.0" 1 "my-app-secure@docker" "http://192.168.1.101:8080" 12ms
    192.168.50.10 - - [17/Apr/2026:14:22:05 +0000] "GET /dashboard HTTP/1.1" 200 4821 "-" "Mozilla/5.0" 1 "my-app-secure@docker" "http://192.168.1.102:8080" 9ms

    Same client, different backends on consecutive requests — that's your smoking gun. To verify whether the cookie is actually being transmitted, capture the traffic on sw-infrarunbook-01:

    tcpdump -i eth0 -A -s 0 'tcp port 80 and host 192.168.50.10' 2>/dev/null | grep -i "^Cookie:"

    If you see no Cookie header in the captured requests, the client isn't sending it back — which points to a path or domain mismatch. If you do see the cookie being sent, decode the value and check whether it maps to a currently live backend:

    python3 -c "import urllib.parse; print(urllib.parse.unquote('http%3A%2F%2F192.168.1.103%3A8080'))"
    
    http://192.168.1.103:8080
    curl -s http://sw-infrarunbook-01:8080/api/http/services/my-app@docker | \
      jq '.loadBalancer.servers'

    If

    192.168.1.103
    isn't in the current server list, the cookie is stale and Traefik can't honor it.

    How to fix it: Use stable DNS names instead of bare IPs for backend URLs wherever possible. With Docker Compose, reference services by their Compose service name — Docker's internal DNS handles resolution and the name stays consistent across restarts. Alternatively, pin your containers to fixed IPs using Docker network

    ipv4_address
    assignments so the cookie value stays valid across restarts. For the path mismatch variant, set
    Path=/
    explicitly on the sticky cookie so it covers the entire application, not just the path that first set it.


    Root Cause 4: SameSite Attribute Blocking

    This one catches engineers off guard because the failure is entirely browser-side — Traefik is doing everything right, but the browser is silently refusing to send the cookie back. No error, no warning in the Traefik logs, just broken stickiness.

    Why it happens: Modern browsers default to

    SameSite=Lax
    when no SameSite attribute is specified in the Set-Cookie header. Under Lax policy, cookies aren't sent on cross-site subrequests — meaning any AJAX call, fetch, or form POST from a page on one origin to your Traefik-fronted backend on a different origin won't include the sticky cookie. If your frontend lives on
    www.solvethenetwork.com
    and your API is behind Traefik on
    api.solvethenetwork.com
    , those are different origins from the browser's perspective, and Lax policy will block the cookie on non-navigational requests.

    With

    SameSite=Strict
    , it's even more aggressive — the cookie is dropped on any cross-site navigation, including link clicks from another domain. With
    SameSite=None
    , the cookie travels on all requests, but browsers then require
    Secure=true
    as a precondition. Omit
    Secure
    with
    SameSite=None
    and the browser rejects the cookie entirely at the point it's set.

    How to identify it: Check what Traefik is currently sending in the Set-Cookie header:

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: STICKY_SESSION=http%3A%2F%2F192.168.1.101%3A8080; Path=/; HttpOnly

    No

    SameSite
    , no
    Secure
    . That configuration will trigger browser blocking in cross-origin scenarios. Open Chrome DevTools on the application, go to Application > Cookies, and look at the SameSite column. Then reproduce the broken flow and watch the Console for warnings like:

    A cookie associated with a cross-site resource at https://api.solvethenetwork.com/ was set without the `SameSite` attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with `SameSite=None` and `Secure`.

    How to fix it: Set the SameSite attribute explicitly to match your deployment topology. For cross-origin setups on HTTPS, use

    None
    with
    Secure
    :

    http:
      services:
        my-app:
          loadBalancer:
            sticky:
              cookie:
                name: STICKY_SESSION
                httpOnly: true
                secure: true
                sameSite: none

    In Docker labels:

    traefik.http.services.my-app.loadbalancer.sticky.cookie.name=STICKY_SESSION
    traefik.http.services.my-app.loadbalancer.sticky.cookie.httponly=true
    traefik.http.services.my-app.loadbalancer.sticky.cookie.secure=true
    traefik.http.services.my-app.loadbalancer.sticky.cookie.samesite=none

    If your application is strictly single-domain and doesn't involve cross-origin requests,

    lax
    is sufficient and more secure. Use
    none
    only when you genuinely need the cookie transmitted cross-site, and only over HTTPS — otherwise you're weakening cookie security for nothing.


    Root Cause 5: Load Balancer Replacing the Cookie

    Traefik is rarely the only proxy in the chain. In production, it's common to have an upstream load balancer — a hardware appliance, an AWS ALB, a GCP HTTPS LB, or even an Nginx reverse proxy — sitting in front of Traefik. If that upstream device does its own cookie-based session persistence, it will overwrite or strip Traefik's sticky cookie before the client ever sees it. The result: Traefik's cookie never reaches the client, and Traefik's stickiness is completely non-functional.

    Why it happens: Enterprise and cloud load balancers inject their own session affinity cookies. AWS ALBs use

    AWSALB
    and
    AWSALBCORS
    . F5 BIG-IP injects
    BIGipServer*
    cookies. Some Nginx configurations use
    proxy_cookie_domain
    or
    proxy_cookie_path
    directives that inadvertently rewrite cookie attributes. Any of these can overwrite Traefik's
    Set-Cookie
    header, replace the cookie name, or strip the cookie entirely as part of a security policy.

    How to identify it: The key is to compare what Traefik emits directly against what the client receives after passing through the upstream. On sw-infrarunbook-01, bypass the upstream and hit Traefik's port directly:

    curl -sv http://127.0.0.1:8081/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: STICKY_SESSION=http%3A%2F%2F192.168.1.101%3A8080; Path=/; HttpOnly; Secure; SameSite=None

    Then make the same request through the upstream load balancer:

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: AWSALB=abc123XYZdef456; Expires=Thu, 24 Apr 2026 14:22:00 GMT; Path=/; SameSite=None

    Traefik sets

    STICKY_SESSION
    . The client receives
    AWSALB
    . The upstream replaced the cookie entirely. You can also look for cases where the upstream adds its own
    Set-Cookie
    line that shadows Traefik's:

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: STICKY_SESSION=http%3A%2F%2F192.168.1.101%3A8080; Path=/; HttpOnly
    < Set-Cookie: AWSALB=abc123XYZdef456; Path=/; SameSite=None

    Two cookies set — but the upstream's cookie is what drives routing at the outer layer, meaning the client may get pinned to a specific Traefik node but Traefik itself still round-robins between backends.

    How to fix it: The cleanest fix is to own session persistence at exactly one layer and disable it everywhere else. If Traefik is the correct layer for stickiness, disable cookie-based persistence on the upstream LB. On an AWS ALB target group, set the stickiness type to

    lb_cookie
    duration-based persistence if you need the outer layer to stay consistent, but configure the ALB to forward cookies rather than generate them. If you're using Nginx as the upstream, audit your config for any
    proxy_cookie_domain
    or
    proxy_cookie_path
    rewrites that might be transforming the cookie Traefik sets. If you have no choice but to keep both layers active, at minimum ensure they use different cookie names so you can trace each layer's behavior independently.


    Root Cause 6: HTTPS Termination and the Secure Cookie Flag

    Your Traefik instance terminates TLS at the edge and proxies traffic over plain HTTP to your backends. The sticky cookie is configured with

    Secure=true
    . In isolation, this is correct — but if any part of your application generates HTTP links internally, or if your entrypoint has an HTTP-to-HTTPS redirect that fires mid-session, browsers will refuse to send the Secure-flagged cookie on the non-HTTPS leg of the request.

    How to identify it: Test the HTTP-to-HTTPS redirect behavior and look at what happens to the cookie:

    curl -sv http://app.solvethenetwork.com/ 2>&1 | grep -iE "location|set-cookie"
    
    < HTTP/1.1 301 Moved Permanently
    < Location: https://app.solvethenetwork.com/

    If a mid-session action triggers an HTTP redirect — an internal link, a form action with a relative URL that resolves to HTTP — the browser makes an HTTP request, doesn't include the Secure cookie, and Traefik treats it as a fresh session.

    How to fix it: Set HSTS headers through Traefik's headers middleware so browsers always upgrade connections before making them:

    http:
      middlewares:
        secure-headers:
          headers:
            stsSeconds: 31536000
            stsIncludeSubdomains: true
            forceSTSHeader: true

    Audit your application for any hardcoded HTTP URLs or relative URLs that could resolve to the HTTP entrypoint. In dev or staging environments where you're running plain HTTP, remove the

    Secure
    flag from the sticky cookie configuration — but set it via an environment-specific Traefik config, not by modifying the production config.


    Root Cause 7: Cookie Domain Mismatch

    The sticky cookie gets set, but its domain scope doesn't cover the hostname of subsequent requests. The browser follows the spec and simply doesn't include the cookie, so Traefik never sees it.

    Why it happens: When Traefik sets a sticky cookie without an explicit domain attribute, the browser scopes it to the exact hostname that set it. A cookie from

    app.solvethenetwork.com
    won't be sent to requests going to
    api.solvethenetwork.com
    . In microservice setups behind a single Traefik instance where the frontend and API share a load balancer but use different
    Host
    headers, this breaks sticky routing silently.

    How to identify it: Check the domain attribute in the Set-Cookie header on an initial request, then watch whether the cookie appears on requests to a different subdomain in your browser's Network tab. Look for requests to

    api.solvethenetwork.com
    that show no Cookie header despite having made a prior request to
    app.solvethenetwork.com
    that set the sticky cookie.

    curl -sv https://app.solvethenetwork.com/ 2>&1 | grep -i "set-cookie"
    
    < Set-Cookie: STICKY_SESSION=http%3A%2F%2F192.168.1.101%3A8080; Path=/; Domain=app.solvethenetwork.com; Secure; HttpOnly

    That domain is too narrow if you're making cross-subdomain API calls.

    How to fix it: The most pragmatic solution is to architect your sticky session configuration so that each subdomain has its own independent service and its own sticky cookie. Don't try to share one sticky cookie across subdomains — it introduces coupling and cookie-scoping complexity. If your application genuinely requires cross-subdomain session state, handle that at the application layer with a shared session store (Redis, Memcached) rather than relying on a single sticky cookie. Session affinity via cookies is a routing aid, not a substitute for proper session management.


    Prevention

    Sticky session failures are configuration failures. Traefik doesn't break them — engineers configure them wrong, usually under time pressure with Docker label soup and implicit naming conventions. The best prevention is building explicit verification into your deployment pipeline rather than relying on users to report breakage.

    After every deployment that touches Traefik configuration, run a quick functional test using curl's cookie jar to simulate a browser session across two requests:

    curl -sc /tmp/sticky_test.txt https://app.solvethenetwork.com/health -o /dev/null -w "%{http_code}"
    curl -sb /tmp/sticky_test.txt https://app.solvethenetwork.com/health -o /dev/null -w "%{http_code}"

    Add an application-level response header like

    X-Served-By
    that identifies the backend instance. Read it on both responses and assert they match. This one check would catch every root cause listed in this article.

    Expose the Traefik API on a non-public port in every environment — even staging — so you can always query the live resolved configuration rather than guessing from labels or YAML files. The mismatch between what you think is configured and what Traefik has actually loaded is the root of most sticky session bugs.

    Use explicit service names in Docker labels. Don't let Traefik auto-generate service names from container and project names — the naming convention is non-obvious and fragile across Compose file changes. Pin the router to a named service, name the service yourself, and apply your sticky config to that name. Three extra labels and you eliminate an entire class of silent misconfiguration.

    Finally: document which proxy layer owns session persistence for each application. In any stack with multiple proxies, there should be exactly one layer responsible for sticky routing. Write that down, ideally in your runbook. The next engineer who touches the infrastructure shouldn't have to reverse-engineer the intent from a pile of load balancer configs and Traefik labels to understand why sessions are or aren't sticky.

    Frequently Asked Questions

    Why is Traefik not setting a sticky session cookie at all?

    The sticky session block is missing from the service's loadBalancer configuration. In Traefik, sticky sessions must be explicitly enabled on the service object — not the router. Check your service definition via the Traefik API at /api/http/services and verify the loadBalancer.sticky block is present. In Docker deployments, make sure your label references the correct service name that the router is actually using.

    Does Traefik sticky session work with Docker Compose?

    Yes, but you need to apply the sticky labels to the service, not the router, and use the correct auto-generated service name. Traefik generates service names using the pattern servicename-projectname@docker. Use the Traefik API to confirm the exact name, then apply your sticky cookie labels to that name — or explicitly declare a service name with traefik.http.routers.myrouter.service=myservice to control the name yourself.

    Why does Traefik sticky session stop working after a container restart?

    The sticky cookie value encodes the backend's URL. If a container restarts and gets a new IP address, the cookie value becomes stale and Traefik can't match it to any live server. Use stable internal DNS names (like Docker Compose service names) instead of bare IPs as your backend server URLs, so the cookie value remains valid across restarts.

    What SameSite value should I use for Traefik sticky session cookies?

    Use SameSite=None with Secure=true for cross-origin deployments (e.g., frontend on one subdomain making API calls to another). Use SameSite=Lax for single-domain applications. Never use SameSite=None without Secure=true — browsers will reject the cookie entirely. Set these explicitly in the Traefik service sticky cookie configuration rather than relying on browser defaults.

    Can an upstream load balancer break Traefik sticky sessions?

    Yes. Upstream load balancers (AWS ALB, Nginx, hardware LBs) often inject their own session persistence cookies and may overwrite or strip Traefik's sticky cookie before it reaches the client. Compare what Traefik emits directly (bypass the upstream and hit Traefik's port) against what the client receives through the full chain. The fix is to own session persistence at exactly one layer and disable it on all others.

    Related Articles