How does session expiration work in a clustered Spring Session (Redis) setup, including timeout config and cleanup?
answer
- maxInactiveInterval default 30 min
- Redis TTL self-expires the key
- Indexed repo + keyspace notifications → SessionExpiredEvent
- JDBC = scheduled cleanupExpiredSessions cron
- notify-keyspace-events Egx required
basics
~20 sEach 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 sEvery 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@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
Know there's a timeout (default 30 min) after which the session expires.
Explain maxInactiveInterval config and that Redis uses TTL while JDBC needs cleanup.
Distinguish RedisSessionRepository vs indexed, keyspace notifications for events, cleanup cron, and event types.
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