Sessions, Cookies & Filters

Session Lifecycle & Timeout

18 min Lesson 3 of 13

Session Lifecycle & Timeout

An HttpSession does not live forever. It is born on demand, carries state across multiple requests, and eventually dies — either because the user explicitly logs out, because the application decides to end it, or because the server reclaims it after a period of inactivity. Understanding this lifecycle precisely is what separates applications that are merely functional from ones that are secure and resource-efficient.

The Four Phases of a Session

  1. Creation — the session is created (a new JSESSIONID is generated and a server-side store entry is allocated).
  2. Active use — requests arrive that resolve to the same session; attributes are read and written.
  3. Idle — no request touches the session for a stretch of time; the server is counting down.
  4. Invalidation — the session is destroyed, either by timeout or by explicit code.
Key distinction: the timeout clock resets on every request that accesses the session, not on every HTTP request to the server. A static asset (image, CSS) typically does not touch HttpSession, so it does not extend the timer.

Creating a Session

HttpServletRequest provides two methods for obtaining a session object:

// Creates a new session if one does not already exist (common default) HttpSession session = request.getSession(); // Only returns an existing session; returns null if none exists HttpSession existing = request.getSession(false);

Prefer getSession(false) in read-only contexts — for example, when checking whether a user is already logged in. Calling plain getSession() on every request creates a session even for anonymous visitors, wasting server memory and potentially confusing your analytics.

Reading Session Metadata

Once you hold a session reference, several methods reveal its lifecycle state:

HttpSession session = request.getSession(false); if (session != null) { String id = session.getId(); // e.g. "A3F9E2..." long created = session.getCreationTime(); // epoch ms long lastAccess = session.getLastAccessedTime(); // epoch ms int maxInactive = session.getMaxInactiveInterval(); // seconds; -1 = never boolean isNew = session.isNew(); // true only on the creating request }

isNew() returns true only on the very first request that created the session — before the client has sent back the session cookie. This is useful for detecting first-time visitors.

Configuring Timeout

The server invalidates an idle session once it has been inactive for maxInactiveInterval seconds. You can control this at three levels, from broadest to most specific:

  1. web.xml (application-wide default):
<!-- web.xml --> <session-config> <session-timeout>30</session-timeout> <!-- minutes --> </session-config>
  1. Programmatically per session (seconds, not minutes):
// Override for this specific session only session.setMaxInactiveInterval(900); // 15 minutes session.setMaxInactiveInterval(-1); // Never expire (dangerous — use carefully)
  1. Spring Boot application.properties:
# Spring Boot — all standard units work server.servlet.session.timeout=30m
Best-practice timeout values: 15–30 minutes for general web applications, 8–10 minutes for banking or anything involving sensitive data. Never set -1 in production unless the session holds no sensitive state — unbounded sessions accumulate on the heap until the server runs out of memory.

Explicit Invalidation

Relying solely on timeout is not enough. A user clicking "Log Out" must trigger immediate invalidation so that another person at the same computer cannot use the back button to regain access.

import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/logout") public class LogoutServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); // destroys the session and all its attributes } // Expire the session cookie on the client side too jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie("JSESSIONID", ""); cookie.setMaxAge(0); cookie.setPath(req.getContextPath() + "/"); cookie.setHttpOnly(true); resp.addCookie(cookie); resp.sendRedirect(req.getContextPath() + "/login"); } }
Always call invalidate() on logout, and then clear the cookie. Invalidating the server-side session alone is not sufficient — the JSESSIONID cookie remains in the browser. While the server will reject it on the next request (the session no longer exists), there is a small window where a network attacker who already captured the token could try to race the server. Explicitly expiring the cookie closes that window.

Session Listeners

The Servlet specification provides a set of listener interfaces that let you react to session lifecycle events without modifying any servlet code. This is the correct way to implement cross-cutting concerns like audit logging, resource cleanup, and metrics.

HttpSessionListener — creation and destruction

import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionListener; import java.util.concurrent.atomic.AtomicInteger; @WebListener public class ActiveSessionCounter implements HttpSessionListener { private static final AtomicInteger count = new AtomicInteger(0); @Override public void sessionCreated(HttpSessionEvent se) { int active = count.incrementAndGet(); System.out.println("Session created: " + se.getSession().getId() + " | Active sessions: " + active); } @Override public void sessionDestroyed(HttpSessionEvent se) { int active = count.decrementAndGet(); System.out.println("Session destroyed: " + se.getSession().getId() + " | Active sessions: " + active); } public static int getActiveCount() { return count.get(); } }

sessionDestroyed fires for both explicit invalidate() calls and timeout-driven expiry, so it is the right hook for cleanup such as closing user-owned resources or writing a logout audit record.

HttpSessionAttributeListener — attribute changes

import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpSessionAttributeListener; import jakarta.servlet.http.HttpSessionBindingEvent; @WebListener public class SessionAuditListener implements HttpSessionAttributeListener { @Override public void attributeAdded(HttpSessionBindingEvent event) { System.out.printf("Session %s: +%s%n", event.getSession().getId(), event.getName()); } @Override public void attributeRemoved(HttpSessionBindingEvent event) { System.out.printf("Session %s: -%s%n", event.getSession().getId(), event.getName()); } @Override public void attributeReplaced(HttpSessionBindingEvent event) { System.out.printf("Session %s: ~%s (old value: %s)%n", event.getSession().getId(), event.getName(), event.getValue()); } }

HttpSessionBindingListener — object self-aware binding

An object can implement HttpSessionBindingListener directly to receive notification when it is placed into or removed from a session — no separate listener class needed:

import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; public class UserPrincipal implements HttpSessionBindingListener { private final String username; public UserPrincipal(String username) { this.username = username; } @Override public void valueBound(HttpSessionBindingEvent event) { System.out.println(username + " bound to session " + event.getSession().getId()); } @Override public void valueUnbound(HttpSessionBindingEvent event) { System.out.println(username + " unbound from session " + event.getSession().getId()); // release any resources the user holds: database cursors, file handles, etc. } public String getUsername() { return username; } }

Using this pattern, when a session is invalidated and the container removes the UserPrincipal attribute, valueUnbound fires automatically — a clean, object-oriented way to release resources without any external wiring.

The Full Lifecycle in Practice

Bringing it all together: a session is created on first login, its timeout is tuned to the security requirements of the application, a listener keeps a live count for the admin dashboard, and logout explicitly invalidates it and purges the cookie. None of these steps requires polling or a background thread — the container drives the entire lifecycle, and your code hooks into the events it cares about.

Summary

  • Use getSession(false) to avoid creating unnecessary sessions.
  • Configure timeout globally in web.xml or application.properties; override per-session with setMaxInactiveInterval().
  • Always call session.invalidate() on logout and expire the cookie.
  • Use HttpSessionListener for creation/destruction hooks and HttpSessionAttributeListener for attribute-level auditing.
  • Implement HttpSessionBindingListener on domain objects that need to self-manage their resources.