Skip to content

Sticky sessions are a smell — when they're the right answer anyway

Stickiness pins a client to one target. That breaks horizontal scaling, biases load, and creates incident shapes that look like 'one host is hot'. The better target is stateless app + Redis. Here is when stickiness is still the right call.

A SaaS team gets a vendor PHP application running in production behind an ALB across three targets. Login works. The session expires after one click — the user logs in, lands on the dashboard, clicks a link, and gets bounced back to the login screen. Half the time. The vendor’s session is in-memory on the PHP host, and the ALB round-robin places successive requests on different targets, each of which has no record of the session. The team turns on ALB stickiness, the symptom disappears, and the incident is “closed.” Six months later one target is running at 90% CPU while the other two are at 30%, because the long-tenured customers have all stuck to the box that was healthy the day they logged in. Stickiness solved the visible problem and created an invisible one. The better answer was always “make the session not live on the host” — but that requires the vendor to ship a code change, and the operations team had to ship a fix on Friday. Here is when each path is the right call.

What’s happening

Sticky sessions on ALB tie a client to a specific target for a window of time. Once the client lands on a target, subsequent requests from that client go to the same target as long as the stickiness window has not expired and the target is healthy.

ALB supports two stickiness modes:

  • Duration-based (load balancer cookie). ALB sets a cookie named AWSALB (and AWSALBCORS for cross-origin). The cookie contains an encoded target identifier and an expiry. Every subsequent request from the same client carries the cookie; ALB reads it and routes to the same target. The window is configurable from 1 second to 7 days. The application does not need to know stickiness exists.

  • Application-based (application cookie). The application sets a cookie (any name you choose). ALB reads the cookie and routes based on its value. This lets the application decide which clients to make sticky and which to round-robin — e.g. an “edit mode” cookie that pins the user to a target only while they hold an in-flight edit.

The problems stickiness creates:

  1. Hot targets. Stickiness is path-dependent. The first target a client lands on stays the target until the cookie expires or the target fails. If a deployment makes one target appear healthier slightly before the others, every client logging in during that window sticks to it. The result is a long-tenured imbalance — one target gets 60% of the traffic, the others 20% each, and the imbalance does not self-heal.

  2. Failed scale-out. When traffic spikes and the autoscaling group adds two new targets, the new targets only get traffic from clients with no cookie (new sessions). Existing logged-in users stay on the old targets. Scaling up does not relieve the existing pressure; it only handles the new pressure on top.

  3. Cookie failure cascade. If the target a client is stuck to goes unhealthy, ALB fails the client over to another target. But the new target has no session for that client. The client either re-authenticates or gets an error, depending on the application’s logic. Users hit this during a deploy or an AZ event and call it a flaky product.

  4. The latency-pinning gotcha. Stickiness routes you back to the same target regardless of where the target is in the cluster. If the target is in a different AZ from your client’s network ingress, you pay the cross-AZ latency on every request. With round-robin and well-distributed clients, the average latency across requests is the AZ-balanced average; with stickiness, your latency is the latency of your target’s AZ.

The better target architecture is stateless application + external session store. The application reads and writes session data to Redis (ElastiCache or self-hosted), keyed by a session ID in a cookie the application itself manages. Any target can serve any request. Stickiness is not needed; round-robin is correct.

When stickiness is the right call anyway:

  • Legacy applications that cannot be modified. A vendor PHP or Java application that stores session in process memory and ships no Redis support. Stickiness is the operational fix while you negotiate the code change with the vendor (or replace the vendor).

  • WebSocket connections, by nature. A WebSocket is a long-lived connection; it is sticky to one target for the life of the connection. ALB stickiness for WebSocket is implicit and correct.

  • Server-side rendered SPAs with per-target in-flight state. Some SSR setups cache the in-flight render on the target. Pinning the client to one target while the SPA is rendering avoids re-rendering on a different target. This is the rare case where stickiness is the right architecture, not a workaround.

  • GPU or specialized targets. A target group of inference servers where each target has a per-client model warmed up. Stickiness keeps the model warm. The application could solve this with explicit routing, but stickiness is a reasonable shortcut.

How to verify

Checking whether stickiness is on for a target group:

aws elbv2 describe-target-group-attributes \
  --target-group-arn "$TG_ARN" \
  --query 'Attributes[?Key==`stickiness.enabled` || Key==`stickiness.type` || Key==`stickiness.lb_cookie.duration_seconds`]'

Confirming the imbalance hypothesis — request count per target:

aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB \
  --metric-name RequestCountPerTarget \
  --dimensions Name=TargetGroup,Value=$TG_NAME Name=LoadBalancer,Value=$LB_NAME \
  --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
  --end-time   $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 \
  --statistics Average

For sticky target groups, the per-target request count should be relatively flat under steady traffic. A spike on one target without a corresponding spike on the others is the hot-target pattern.

The faster check for hot-target imbalance is target-group access logs forwarded to CloudWatch Insights:

fields @timestamp, target_status_code, target_ip
| stats count(*) as request_count by target_ip
| sort request_count desc

A healthy round-robin distribution looks like every target with within 10% of the median. An imbalanced sticky distribution looks like one target at 2-3x the median.

Confirming the cross-AZ latency cost — TargetResponseTime per target:

aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB \
  --metric-name TargetResponseTime \
  --dimensions Name=TargetGroup,Value=$TG_NAME Name=LoadBalancer,Value=$LB_NAME Name=AvailabilityZone,Value=us-east-1a \
  --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
  --end-time   $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 \
  --extended-statistics p95

Compare across AZs. If one AZ’s targets are consistently slower for the same load, the sticky clients in that AZ are paying for it.

The fix

The decision flow:

  1. Default to round-robin (no stickiness). Most stateless modern applications work this way and should keep working this way.

  2. If a session is required, externalize it. Redis (ElastiCache), DynamoDB, or even a database table. The session-store call adds a few milliseconds per request; that is a fair price for a fleet that scales horizontally.

  3. If you cannot externalize (vendor app, immediate deploy), turn on stickiness — but cap the duration aggressively. A 24-hour stickiness window guarantees long-tail imbalance. A 1-hour window lets clients re-distribute as targets cycle. Pick the shortest window that keeps the user experience acceptable.

  4. Monitor the imbalance. A CloudWatch alarm on RequestCountPerTarget standard deviation, or a daily report of per-target request counts. The alarm threshold is set empirically; the principle is that you want to see hot targets, not discover them on Friday.

  5. Plan the migration off stickiness as a real project. Externalize the session store, update the application to read/write it, deploy with stickiness still on, then turn stickiness off and confirm that round-robin works. Do not do this in the same change as the application update — separate the rollout.

Three rules we hold:

Stickiness is operational, not architectural. A system that needs stickiness to function has a state problem one layer down. Stickiness buys time to fix it.

Short duration, application-cookie when possible. A 1-hour load-balancer-cookie window and your application controls when a request is sticky and when it is not. The default 7-day duration cookie is almost always too long.

Watch the imbalance metric, not the average latency. Average latency hides the hot-target pattern. The fleet looks healthy on the dashboard while one target is grinding.

For more on health checks (which determine whether a target is in the rotation in the first place), see Health check tuning for SaaS targets. For ALB listener rule patterns that interact with sticky target groups, see ALB listener rules — host and path routing.


Stack Harbor designs ALB + NLB topologies and the operating model around them as part of Managed AWS onboarding and ongoing Managed Cloud Ops. If your fleet has one hot target and three idle ones, start with an infrastructure consult.