skip to content

How does session expiration and cleanup work across the Redis and JDBC backends, and what Redis configuration is required for expiration events?

level: seniorimportance: should knowfreq 42%

answer

  1. Redis TTL is lazy — needs sorted-set index + background task
  2. notify-keyspace-events Egx or events don't fire
  3. Indexed = events + principal index; non-indexed = pure TTL
  4. JDBC: no TTL, @Scheduled cleanupCron default every minute
  5. SPRING_SESSION + SPRING_SESSION_ATTRIBUTES, EXPIRY_TIME

basics

~20 s

Redis expires session keys via its own TTL, and RedisIndexedSessionRepository uses keyspace notifications plus a cleanup job to fire expiration events. JDBC has no TTL, so JdbcIndexedSessionRepository runs a scheduled task that deletes expired rows.

solid answer

~40 s

Each backend enforces the session timeout differently. With RedisIndexedSessionRepository, each session is stored with a Redis TTL, but Redis lazily evicts keys, so to fire SessionExpiredEvent/SessionDeletedEvent reliably Spring Session also keeps a sorted set of expiration times and runs a background task, and it relies on Redis keyspace notifications — you must enable notify-keyspace-events (at least Egx) or expiration events won't be delivered. The newer RedisSessionRepository (non-indexed) uses pure TTL with no index and no reliable events. JDBC has no native expiry: JdbcIndexedSessionRepository stores EXPIRY_TIME columns and a @Scheduled cleanup task (default cron: top of every minute, configurable via cleanupCron) deletes rows past their expiry. In all cases lastAccessedTime is updated per request and maxInactiveInterval defines the timeout.

code

java · 18 lines
java
// JDBC: override the cleanup cron (default is every minute)
@Configuration
@EnableJdbcHttpSession(cleanupCron = "0 */5 * * * *") // every 5 minutes
public class JdbcSessionConfig { }

// Redis: ensure keyspace notifications so expiration events fire.
// Spring tries this automatically; on managed Redis set it server-side:
//   redis.conf ->  notify-keyspace-events Egx
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
class RedisSessionConfig {

    @EventListener
    void onExpired(SessionExpiredEvent event) {
        // fires only if keyspace notifications are enabled
        log.info("Session expired: {}", event.getSessionId());
    }
}

go deeper

for a junior

Know a session times out after maxInactiveInterval (default 30 min) and old sessions get cleaned up.

for a middle

Distinguish Redis TTL from the JDBC scheduled cleanup and know the default JDBC cleanup runs every minute.

for a senior

Explain the Redis expiration index + keyspace-notification requirement and the indexed vs non-indexed repositories.

for a principal

Diagnose managed-Redis event failures, tune JDBC cleanup for table growth/lock contention, and choose the repository type per event/lookup needs.

## The core model Every Spring Session `Session` carries a **`maxInactiveInterval`** (the idle timeout, default 30 minutes) and a **`lastAccessedTime`** that the `SessionRepositoryFilter` refreshes on each request. Expiration means: `now - lastAccessedTime > maxInactiveInterval`. How that gets *enforced and cleaned up* differs sharply by backend, and this is where interview depth lives. ## Redis — `RedisIndexedSessionRepository` (@EnableRedisHttpSession) Redis can expire keys natively, but there are subtleties: 1. **Per-session TTL.** The session hash is stored with a Redis **TTL** set slightly beyond the max-inactive interval. When Redis evicts the key, the session is gone. 2. **Redis eviction is lazy/probabilistic.** Redis does not guarantee a key is deleted the instant it expires — it's removed on access or by a background sampling job. That means relying on TTL alone would *delay* the `SessionExpiredEvent`. 3. **The expiration index.** To fire events promptly, Spring Session maintains a **sorted set** of session expiration timestamps (`spring:session:expirations`) and runs a background task that, each minute, looks up sessions due to expire and *touches* their keys, forcing Redis to evaluate expiry and emit a keyspace event. 4. **Keyspace notifications are mandatory for events.** For `SessionDeletedEvent` and `SessionExpiredEvent` to be delivered, Redis must have **`notify-keyspace-events`** configured to include generic + expired + evicted events — commonly **`Egx`** (or `KEA`). If it's off, sessions still expire but your application-level event listeners never fire. Spring Session tries to set this via `ConfigureNotifyKeyspaceEventsAction` at startup, but on managed Redis (where `CONFIG SET` is blocked) you must set it in the server config yourself. 5. **Indexing.** `RedisIndexedSessionRepository` also maintains a principal-name index, enabling `FindByIndexNameSessionRepository` lookups (find all sessions for a user) — the basis for Spring Security's concurrent-session control. 6. **Non-indexed alternative.** `RedisSessionRepository` (selected via `spring.session.redis.repository-type=default` in Boot) uses pure TTL, has **no** expiration index and **no** reliable expiration events, but is lighter and doesn't need keyspace notifications. Choose it if you don't need events or per-principal lookups. ## JDBC — `JdbcIndexedSessionRepository` (@EnableJdbcHttpSession) A relational database has **no native TTL**, so cleanup is explicit: - Sessions live in **`SPRING_SESSION`** (with `EXPIRY_TIME`, `LAST_ACCESS_TIME`, `MAX_INACTIVE_INTERVAL`, `PRINCIPAL_NAME`) and attributes in **`SPRING_SESSION_ATTRIBUTES`** (BLOB values). - A **`@Scheduled`** cleanup task, `cleanupExpiredSessions()`, runs on a cron — **default `0 * * * * *` (top of every minute)** — and issues a `DELETE` for rows whose `EXPIRY_TIME` is in the past. You override it with the annotation's **`cleanupCron`** attribute (or `spring.session.jdbc.cleanup-cron`). - Because deletion is periodic, an expired session can linger in the table until the next sweep, though queries by id already treat it as expired. - On startup the schema is created from a backend-specific DDL script (e.g. `schema-postgresql.sql`) unless you disable initialization. ## Hazelcast (brief) `@EnableHazelcastHttpSession` stores sessions in an `IMap` configured with `max-idle-seconds`, and registers a `MapListener`/entry-expiry listener to fire the Spring Session events. ## Gotchas to name in an interview - **Managed Redis + keyspace notifications:** the #1 production surprise — expiration events silently don't fire because `notify-keyspace-events` wasn't set and `CONFIG SET` is blocked. - **JDBC cleanup lag & lock contention:** a slow or blocked cleanup job lets the table grow; a huge single DELETE can contend. Tune `cleanupCron`. - **maxInactiveInterval vs cookie expiry:** the server-side timeout and the cookie's own max-age are separate; a persistent cookie doesn't extend the server session. - **RedisSessionRepository vs RedisIndexedSessionRepository:** picking the non-indexed one and then expecting `SessionExpiredEvent` or per-user lookups to work.

  • Your SessionExpiredEvent listener never fires on a managed Redis (AWS ElastiCache). Why, and how do you fix it?
    Expiration events depend on Redis keyspace notifications, but managed Redis often blocks CONFIG SET, so Spring Session's ConfigureNotifyKeyspaceEventsAction can't enable them at startup. Set notify-keyspace-events (e.g. Egx / KEA) in the parameter group/server config manually. Sessions still expire via TTL either way — only the events are missing.
  • How does the JDBC backend get rid of expired sessions given a database has no TTL?
    JdbcIndexedSessionRepository runs a @Scheduled task (cleanupExpiredSessions, default cron '0 * * * * *') that DELETEs rows from SPRING_SESSION whose EXPIRY_TIME has passed. You tune it with cleanupCron. Until the sweep runs the expired row remains, but lookups by id already treat it as expired.

saying these in an interview costs you the question

  • Claiming Redis deletes expired keys instantly and thus events always fire
  • Not knowing notify-keyspace-events is required for expiration events
  • Thinking the JDBC backend expires rows automatically without a scheduled job
  • Confusing RedisSessionRepository (non-indexed, no events) with RedisIndexedSessionRepository
  • Believing the session cookie's max-age controls server-side expiry

context