skip to content

Concurrent Session Control

Limiting a user to N sessions requires a registry and a filter, and you must choose between rejecting the new login and expiring the oldest. Interviewers ask what happens to that registry once you run more than one instance.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is the difference between maxSessionsPreventsLogin(true) and (false)?

level: middleimportance: must knowfreq 55%

answer

  1. false = newest wins, oldest expired
  2. true = oldest wins, new login rejected
  3. true throws SessionAuthenticationException
  4. true + stale registry = lockout risk
  5. limit is per-principal (equals/hashCode)

basics

~20 s

With false (default) a new login is allowed and the oldest session is expired. With true the new login is refused (throws SessionAuthenticationException) and existing sessions stay alive — old sessions win over new ones.

solid answer

~40 s

`maxSessionsPreventsLogin` decides who loses when a user exceeds `maximumSessions`. Default `false`: the new authentication succeeds and Spring's `ConcurrentSessionControlAuthenticationStrategy` marks the oldest session's `SessionInformation` as expired; that session is torn down by `ConcurrentSessionFilter` on its next request ("kick the old device"). With `true`: the strategy throws `SessionAuthenticationException` at login time, so the new attempt is rejected and current sessions are untouched ("first-in wins"). Choose `true` for strict single-seat licensing where you never want an active user silently kicked; choose `false` for consumer UX where the latest device should just take over. A caveat with `true`: if a previous session is still counted (e.g. the registry wasn't told it was destroyed), a legitimate user can be locked out — which is why correct `HttpSessionEventPublisher` wiring matters.

code

java · 9 lines
java
http.sessionManagement(session -> session
    .maximumSessions(1)
    .maxSessionsPreventsLogin(true)   // reject the new login instead of
                                      // expiring the existing session
    .expiredUrl("/login?expired"));

// With true, a second concurrent login triggers:
//   org.springframework.security.web.authentication.session
//     .SessionAuthenticationException: "Maximum sessions of 1 ... exceeded"

go deeper

for a junior

Know false = new login wins, true = new login blocked.

for a middle

Explain the mechanism: expireNow vs SessionAuthenticationException, and the lazy expiry on next request.

for a senior

Tie the true-mode lockout risk to HttpSessionEventPublisher and registry accuracy; discuss per-principal equals/hashCode.

for a principal

Weigh UX and security trade-offs, and note both modes depend on a reliable, ideally shared, SessionRegistry in a cluster.

**The setting.** `maxSessionsPreventsLogin` is a boolean on `sessionManagement().maximumSessions(n)`. It only matters when a login would push a principal *over* the `maximumSessions` limit. **`false` — the default (newest wins).** - The login is allowed to proceed. - `ConcurrentSessionControlAuthenticationStrategy` finds the principal's least-recently-used session in the `SessionRegistry` and calls `SessionInformation.expireNow()`, flipping its `expired` flag to true. - The old session is *not* immediately invalidated server-side; it dies lazily. On that old session's **next request**, `ConcurrentSessionFilter` sees `sessionInformation.isExpired()`, invalidates the `HttpSession`, runs the logout handlers, and redirects to the configured `expiredUrl` (or writes a default expired message). - User-visible effect: logging in on a new device silently boots the oldest device. **`true` (oldest wins / login prevented).** - At login, `ConcurrentSessionControlAuthenticationStrategy` throws `SessionAuthenticationException` ("Maximum sessions of N for this principal exceeded"). - The new authentication fails; existing sessions are untouched. - User-visible effect: the second device sees a login error until an existing session logs out or times out. **Why the wiring matters more with `true`.** The decision is based on the *count in the registry*. If a session ended (browser closed, timeout, logout) but the registry was never notified, its stale entry still counts toward the limit. With `true` that stale count can lock a legitimate user out entirely. The fix is registering `HttpSessionEventPublisher` so `HttpSessionDestroyedEvent` removes ended sessions from `SessionRegistryImpl`. With `false` a stale entry is less harmful because the old (possibly dead) session just gets expired. **Edge cases.** - The limit is per-principal, keyed by the principal's `equals`/`hashCode`. If your custom principal object doesn't implement these correctly, counting breaks. - With `true`, there is no "queue": it's a hard rejection, not a wait. - Setting `maximumSessions` at all activates the concurrency filter/strategies even if you leave `maxSessionsPreventsLogin` at default. **When to use which.** `true`: strict license seats, security-sensitive apps that must never silently drop an authenticated session, admin consoles. `false`: consumer apps where 'log in on my new phone and it just works' is the expected UX.

  • With maxSessionsPreventsLogin(true), how can a legitimate user get wrongly locked out, and how do you prevent it?
    If a prior session ended but the SessionRegistry still counts it (no destroy event), the count stays at the cap. Registering HttpSessionEventPublisher so HttpSessionDestroyedEvent purges the registry prevents this.
  • Is the old session invalidated immediately when maxSessionsPreventsLogin is false?
    No. It is only flagged expired in the registry; ConcurrentSessionFilter invalidates it lazily on that session's next request.

saying these in an interview costs you the question

  • Saying true expires the old session (it actually blocks the new login)
  • Claiming the old session is killed instantly rather than on its next request
  • Not knowing which is the default (false is default)

context

open as a page

Why is HttpSessionEventPublisher required for concurrent session control, and what breaks without it?

level: seniorimportance: must knowfreq 50%

basics

~20 s

HttpSessionEventPublisher is a servlet listener that turns container session create/destroy events into Spring events so SessionRegistryImpl knows when sessions end. Without it, ended sessions stay counted in the registry and users can be falsely locked out.

open as a page

What is concurrent session control in Spring Security and how do you limit a user to one active session?

level: juniorimportance: should knowfreq 45%

basics

~10 s

Concurrent session control limits how many simultaneous HTTP sessions one authenticated user can have. In the security config you call sessionManagement().maximumSessions(1) so a second login is either blocked or expires the older session.

open as a page

Walk through the components that enforce concurrent session control: SessionRegistry, the authentication strategies, and ConcurrentSessionFilter.

level: seniorimportance: should knowfreq 40%

basics

~20 s

At login, ConcurrentSessionControlAuthenticationStrategy checks the SessionRegistry count and enforces the limit, then RegisterSessionAuthenticationStrategy records the new session. On every request, ConcurrentSessionFilter checks if the session was marked expired and, if so, logs out and redirects.

open as a page

Why does default concurrent session control break in a clustered/multi-instance deployment, and how do you fix it?

level: principalimportance: should knowfreq 30%

basics

~20 s

The default SessionRegistryImpl is an in-memory map local to one JVM, so each node counts only its own sessions. Across nodes the limit isn't truly enforced. Fix it with a shared session store like Spring Session (Redis/JDBC) and SpringSessionBackedSessionRegistry.

open as a page