Skip to main content
the auth layer

Session Management at Scale: Distributed Sessions, Redis, and Logout That Actually Works

4 min read Chapter 19 of 45

Session Management at Scale

Stateless authentication with JWTs shifts complexity from the server to the token. Stateful authentication with sessions shifts complexity from the token to the infrastructure. Neither eliminates complexity. They relocate it.

In the multi-tenant SaaS platform, the frontend shell uses sessions for browser clients (cookie-based, HTTP-only, instant invalidation on logout). The decision from CH6 was clear: opaque tokens for browser-facing APIs, backed by server-side state. That server-side state is the session.

A single application server with in-memory sessions works until the second instance starts. Sticky sessions (load balancer routes the same user to the same server) work until a deployment. Blue-green deployments create new instances. Rolling deployments terminate old instances. In both cases, sessions bound to a specific instance are lost.

Why Distributed Sessions

The failure mode is concrete. User Alice is authenticated on instance A. A deployment starts. Instance A is drained and terminated. Alice’s next request hits instance B. Instance B has no record of Alice’s session. Alice is redirected to the login page mid-workflow. She loses unsaved form data. Her SaaS experience is degraded.

Distributed sessions solve this by storing session data in a shared store (Redis) accessible to all instances. Any instance can validate any session. Deployments do not destroy sessions. Scale-out events do not fragment session data.

The cost: a network round-trip to Redis on every request that touches the session. For the multi-tenant platform, this adds 1-3ms per request at the P50 (Redis on the same VPC), which is acceptable for browser-facing APIs where latency budgets are 200-500ms.

The Architecture

Distributed session architecture: browser cookie flows through load balancer to any app instance, all backed by a shared Redis cluster

The diagram shows the stateless instance model: the load balancer routes requests to any available instance without sticky sessions. Each instance reads session data from Redis on every request, adding 1-3ms of latency but enabling zero-downtime deployments and horizontal scaling. The __Host-SESSION cookie prefix enforces that the cookie is bound to the exact origin (requires Secure, forbids Domain attribute), preventing cookie tossing attacks from subdomains.

Key architectural decisions:

  1. Cookie transport. The session ID is transmitted in a cookie with Secure, HttpOnly, SameSite=Lax attributes. Not in a URL parameter (session hijacking via referer leakage), not in a custom header (requires JavaScript access, defeating HttpOnly).

  2. Redis as session store. Not Hazelcast, not a database. Redis because: sub-millisecond reads, built-in TTL for automatic expiry, pub/sub for session events, atomic operations for concurrent session control.

  3. Indexed sessions. Spring Session’s FindByIndexNameSessionRepository maintains indexes by principal name, allowing “find all sessions for user X” (needed for concurrent session limits and forced logout).

Spring Session Configuration

@Configuration
@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 minutes
public class SessionConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("redis.internal.saas.example");
        config.setPort(6379);
        config.setPassword(RedisPassword.of(System.getenv("REDIS_PASSWORD")));
        config.setDatabase(1); // Dedicated database for sessions

        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .useSsl()
            .build();

        return new LettuceConnectionFactory(config, clientConfig);
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("__Host-SESSION"); // __Host- prefix enforces Secure + path=/
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite("Lax");
        serializer.setCookiePath("/");
        serializer.setDomainName("app.saas.example");
        return serializer;
    }
}

The __Host- cookie name prefix is a security mechanism (RFC 6265bis). Cookies with this prefix are required to be set with Secure, without a Domain attribute, and with Path=/. Browsers enforce these constraints, preventing subdomain cookie injection attacks where an attacker at evil.saas.example sets a cookie that the browser sends to app.saas.example.

Session Security Properties

PropertyConfigurationThreat mitigated
HttpOnlysetUseHttpOnlyCookie(true)XSS cannot read the session cookie
SecuresetUseSecureCookie(true)Session cookie not sent over HTTP
SameSite=LaxsetSameSite("Lax")CSRF via cross-origin POST (partial)
__Host- prefixsetCookieName("__Host-SESSION")Subdomain cookie injection
30-min inactivity timeoutmaxInactiveIntervalInSeconds = 1800Abandoned sessions expire
Session fixation preventionchangeSessionId()Attacker cannot fixate a session ID

Each property defends against a specific attack. Removing any one opens a specific vulnerability. The combination provides defense-in-depth for the session transport layer.

What This Chapter Covers

Section 1 covers the session store internals: serialization (why JDK serialization is a remote code execution vector), key structure in Redis, TTL management, and the hardened serialization configuration.

Section 2 covers session lifecycle attacks and controls: session fixation (the attack that makes pre-authentication session IDs dangerous), concurrent session limits (preventing credential sharing), and logout propagation (how to make “Sign out everywhere” actually work across a distributed system).