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
stickyblock 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@dockeror
name-projectname@dockerconvention. If you define sticky session labels on a service named
my-appbut 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
Pathattribute doesn't match the path of the subsequent requests — the browser won't include a cookie scoped to
/apion 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.103isn'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_addressassignments 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=Laxwhen 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.comand 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=trueas a precondition. Omit
Securewith
SameSite=Noneand 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
Nonewith
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,
laxis sufficient and more secure. Use
noneonly 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
AWSALBand
AWSALBCORS. F5 BIG-IP injects
BIGipServer*cookies. Some Nginx configurations use
proxy_cookie_domainor
proxy_cookie_pathdirectives that inadvertently rewrite cookie attributes. Any of these can overwrite Traefik's
Set-Cookieheader, 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-Cookieline 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_cookieduration-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_domainor
proxy_cookie_pathrewrites 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
Secureflag 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.comwon'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
Hostheaders, 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.comthat show no Cookie header despite having made a prior request to
app.solvethenetwork.comthat 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-Bythat 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.
