skip to content

How does session expiration work in a clustered Spring Session (Redis) setup, including timeout config and cleanup?

level: seniorimportance: should knowfreq 44%

answer

  1. maxInactiveInterval default 30 min
  2. Redis TTL self-expires the key
  3. Indexed repo + keyspace notifications → SessionExpiredEvent
  4. JDBC = scheduled cleanupExpiredSessions cron
  5. notify-keyspace-events Egx required

basics

~20 s

Each session has a max inactive interval (default 30 min). With Redis, the session key gets a TTL so it self-expires; an indexed repository additionally tracks expirations to fire SessionDeleted/SessionExpiredEvents. JDBC needs a scheduled cleanup job.

solid answer

~40 s

Every Spring Session `Session` has a **max inactive interval** — idle time before expiry — configurable via `server.servlet.session.timeout` (Boot) or the enabling annotation's `maxInactiveIntervalInSeconds`, defaulting to 30 minutes; `lastAccessedTime` is refreshed on each access. Backends differ in enforcement. **Redis** sets a **TTL** on the session key so it self-expires without a scan. But TTL expiry doesn't guarantee a timely event, so `RedisIndexedSessionRepository` maintains an extra expiration index and uses key-space notifications plus a background cleanup to reliably delete and to publish `SessionDeletedEvent` / `SessionExpiredEvent`. The lightweight `RedisSessionRepository` relies purely on TTL and doesn't fire expiration events. **JDBC** can't self-expire rows, so `JdbcIndexedSessionRepository` runs a scheduled `cleanupExpiredSessions()` (default cron, hourly-ish) to delete stale rows. Events let Spring Security's concurrency control and app listeners react to logouts/expiry across the cluster.

code

java · 17 lines
java
@Configuration
@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 min, indexed = events
public class SessionConfig {

    // Redis must have keyspace notifications on for SessionExpiredEvent to fire.
    // In redis.conf: notify-keyspace-events Egx

    @EventListener
    public void onExpired(SessionExpiredEvent e) {
        log.info("Session {} expired via timeout", e.getSessionId());
    }

    @EventListener
    public void onDeleted(SessionDeletedEvent e) {
        log.info("Session {} deleted (logout/invalidate)", e.getSessionId());
    }
}

go deeper

for a junior

Know there's a timeout (default 30 min) after which the session expires.

for a middle

Explain maxInactiveInterval config and that Redis uses TTL while JDBC needs cleanup.

for a senior

Distinguish RedisSessionRepository vs indexed, keyspace notifications for events, cleanup cron, and event types.

for a principal

Reason about reliability of expiry semantics across the cluster, event-driven logout-everywhere, and operational config (notify-keyspace-events, cron) as failure points.

## The expiration model Every Spring Session `Session` carries: - **`getMaxInactiveInterval()`** — a `Duration` of allowed idle time. Default **1800 seconds (30 min)**. `0` or negative means *never expires*. - **`getLastAccessedTime()`** — refreshed each time the session is accessed (a request touches it). - **`isExpired()`** — true when `now - lastAccessedTime > maxInactiveInterval`. ### Configuring the timeout - **Spring Boot:** `server.servlet.session.timeout=30m` (a `Duration`; a bare number is seconds, minimum granularity 1 minute for cookies historically). This is the canonical Boot property and Spring Session honors it. - **Annotation:** `@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)` or `@EnableJdbcHttpSession(...)`. ## Why backends behave differently Expiration in a *distributed* store is harder than in-memory because no single JVM owns a timer per session. ### Redis Two repositories exist: - **`RedisSessionRepository`** (the newer, simpler default in recent versions): stores the session as a Redis hash and sets a **TTL** (`EXPIRE`) so Redis itself evicts the key when idle time passes. Simple and low-overhead, but **fires no `SessionExpiredEvent`** and offers **no secondary indexes** (so no 'find all sessions for a principal'). - **`RedisIndexedSessionRepository`**: adds machinery for reliable events and lookups: - A **secondary expiration index** (a sorted set / round-to-the-minute buckets) tracking when sessions should die. - Relies on Redis **keyspace notifications** (`notify-keyspace-events` must include `Egx` for generic + expired events). When a key expires, Redis emits an event that the repository catches to delete the session and publish **`SessionExpiredEvent`** (or `SessionDeletedEvent` on explicit invalidate). - A background task processes the expiration index because Redis only *lazily* removes expired keys (it won't send the notification until the key is accessed or actively expired), so the index nudges Redis to actually expire keys near their deadline. - Enables **`findByIndexNameAndIndexValue`** (e.g. all sessions for a username) — needed by Spring Security concurrent-session control and 'log out everywhere'. **Gotcha:** if you rely on `SessionExpiredEvent` but Redis keyspace notifications are disabled (`notify-keyspace-events` empty), events won't fire — a classic production surprise. ### JDBC Rows in a table can't self-expire. `JdbcIndexedSessionRepository` schedules **`cleanupExpiredSessions()`** (a `@Scheduled` cron, default around top-of-hour) that `DELETE`s rows whose `EXPIRY_TIME` has passed. Until cleanup runs, expired rows still exist but are treated as expired on read. Set the cron via `spring.session.jdbc.cleanup-cron`. ### Hazelcast Uses an entry TTL / `MapListener` with `EntryExpiredListener` to fire events. ## Events and who consumes them Spring Session publishes `SessionCreatedEvent`, `SessionDeletedEvent` (explicit invalidation), and `SessionExpiredEvent` (timeout). Consumers: - **Spring Security** concurrent-session control / `SessionRegistry` to keep its view accurate across the cluster. - App `@EventListener`s for cleanup (releasing resources, audit). - WebSocket teardown (see WebSocket integration): expiring the HTTP session can disconnect associated WebSocket sessions. ## Edge cases & gotchas - **`session.invalidate()` / logout** produces a `SessionDeletedEvent`, distinct from timeout's `SessionExpiredEvent`. - **Clock skew** across instances is irrelevant for Redis TTL (Redis is authoritative) but matters conceptually for last-accessed comparisons — keep it in mind. - **Changing the timeout** only affects TTL on next save; existing keys keep their prior TTL until touched. - **Never expires (`0`)** means keys live forever — a memory leak in Redis; avoid unless intentional. - **`RedisSessionRepository` vs `RedisIndexedSessionRepository`** choice is significant: pick the indexed one if you need events or per-principal lookups; you may need to opt in via configuration since the simpler one can be the default. ## When to care Any clustered login system where you need reliable logout-everywhere, session-count limits, or cleanup of abandoned carts. Getting keyspace notifications / cleanup cron wrong silently breaks expiry semantics.

  • Why might SessionExpiredEvent never fire in a Redis setup even though sessions do disappear?
    Redis keyspace notifications are off. Without notify-keyspace-events including expired ('gx'/'Ex') events, RedisIndexedSessionRepository never receives the expiry notification to publish the event — the TTL still evicts the key, so it silently vanishes with no event.
  • How does expiration cleanup differ between the Redis and JDBC backends?
    Redis uses a per-key TTL so the store self-evicts (indexed repo adds a background nudge + events). JDBC rows can't self-expire, so JdbcIndexedSessionRepository runs a scheduled cleanupExpiredSessions() cron to DELETE stale rows.

saying these in an interview costs you the question

  • Thinking Redis fires expiration events automatically without keyspace notifications enabled
  • Assuming JDBC sessions self-expire like Redis TTL (they need the cleanup job)
  • Confusing SessionDeletedEvent (invalidate/logout) with SessionExpiredEvent (timeout)
  • Believing lastAccessedTime isn't refreshed per request

context