Durable Sessions
When a server restarts, all in-memory state is lost — room memberships, broadcaster subscriptions, metadata. Durable sessions persist this state and restore it automatically when a client reconnects.
How It Works
Section titled “How It Works”The durable session lifecycle is driven by the DurableSessionInterceptor:
- First connect — the interceptor creates a
DurableSession, saves it via theSessionStore, and returns a token to the client in theX-Atmosphere-Session-Tokenresponse header. - During the connection — the interceptor registers a disconnect listener that captures the resource’s current rooms and broadcaster subscriptions into the session store.
- Reconnect — if the client sends the token back in the
X-Atmosphere-Session-Tokenrequest header (or as a query parameter), the interceptor restores the session: it re-joins the resource to its previous broadcasters and rooms usingBroadcasterFactory.lookup()andRoomManager.room().join(). - Expiration — a background thread periodically calls
store.removeExpired(ttl)to clean up sessions that have not been seen within the configured TTL (default: 24 hours).
The DurableSession Record
Section titled “The DurableSession Record”DurableSession is a Java record that captures the full state snapshot:
public record DurableSession( String token, String resourceId, Set<String> rooms, Set<String> broadcasters, Map<String, String> metadata, Instant createdAt, Instant lastSeen) { }It provides factory and copy methods:
DurableSession.create(token, resourceId)— creates a new session with the current timestampwithRooms(Set<String>)— returns a copy with updated roomswithBroadcasters(Set<String>)— returns a copy with updated broadcaster IDswithMetadata(Map<String, String>)— returns a copy with updated metadatawithResourceId(String)— returns a copy with a new resource ID and refreshedlastSeen
The SessionStore SPI
Section titled “The SessionStore SPI”The SessionStore interface defines the persistence contract:
public interface SessionStore { void save(DurableSession session); Optional<DurableSession> restore(String token); void remove(String token); void touch(String token); List<DurableSession> removeExpired(Duration ttl); default void close() { }}Three implementations are provided:
| Implementation | Module | Description |
|---|---|---|
InMemorySessionStore | durable-sessions | ConcurrentHashMap-backed, for testing and development. Sessions are lost on restart. |
SqliteSessionStore | durable-sessions-sqlite | Embedded SQLite database, zero-config. Perfect for single-node deployments. |
RedisSessionStore | durable-sessions-redis | Lettuce-backed Redis store. Shared across cluster nodes. |
InMemorySessionStore
Section titled “InMemorySessionStore”The default store when no other is configured. Uses a ConcurrentHashMap and is only suitable for development and testing since sessions are lost when the JVM exits.
SqliteSessionStore
Section titled “SqliteSessionStore”Zero-configuration embedded storage. Just add the dependency:
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-durable-sessions-sqlite</artifactId> <version>${project.version}</version></dependency>SqliteSessionStore creates a SQLite database file with a durable_sessions table using WAL journal mode for concurrent read performance. Three constructors are available:
var store = new SqliteSessionStore(); // default: atmosphere-sessions.dbvar store = new SqliteSessionStore(Path.of("/data/sessions")); // custom pathvar store = SqliteSessionStore.inMemory(); // for testingParent directories are created automatically if they do not exist.
RedisSessionStore
Section titled “RedisSessionStore”For clustered deployments where multiple nodes need access to the same session data:
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-durable-sessions-redis</artifactId> <version>${project.version}</version></dependency>RedisSessionStore uses Lettuce and stores sessions as JSON hashes with a TTL.
Spring Boot Integration
Section titled “Spring Boot Integration”The atmosphere-spring-boot-starter auto-configures durable sessions when a SessionStore bean is present. The sample application at samples/spring-boot-durable-sessions/ demonstrates the full setup.
SessionStoreConfig.java
Section titled “SessionStoreConfig.java”From samples/spring-boot-durable-sessions/:
@Configurationpublic class SessionStoreConfig {
@Bean public SessionStore sessionStore() { return new SqliteSessionStore(Path.of("data/sessions.db")); }}That is the only configuration needed. The auto-configuration picks up the SessionStore bean and registers the DurableSessionInterceptor automatically.
Chat.java
Section titled “Chat.java”The @ManagedService class does not need any durable-session-specific code. The interceptor handles everything transparently:
@ManagedService(path = "/atmosphere/chat", atmosphereConfig = MAX_INACTIVE + "=120000")public class Chat {
private final Logger logger = LoggerFactory.getLogger(Chat.class);
@Inject private AtmosphereResource r;
@Inject private AtmosphereResourceEvent event;
@Ready public void onReady() { logger.info("Client {} connected (session will persist across restarts)", r.uuid()); }
@Disconnect public void onDisconnect() { if (event.isCancelled()) { logger.info("Client {} unexpectedly disconnected — session saved", event.getResource().uuid()); } else { logger.info("Client {} closed — session saved", event.getResource().uuid()); } }
@org.atmosphere.config.service.Message( encoders = {JacksonEncoder.class}, decoders = {JacksonDecoder.class}) public Message onMessage(Message message) { logger.info("{} says: {}", message.getAuthor(), message.getMessage()); return message; }}DurableSessionsApplication.java
Section titled “DurableSessionsApplication.java”The main class is a standard Spring Boot application:
@SpringBootApplicationpublic class DurableSessionsApplication { public static void main(String[] args) { SpringApplication.run(DurableSessionsApplication.class, args); }}Programmatic Registration
Section titled “Programmatic Registration”If you are not using Spring Boot auto-configuration, register the interceptor manually:
var store = new SqliteSessionStore(Path.of("data/sessions.db"));framework.interceptor(new DurableSessionInterceptor(store));The constructor also accepts optional TTL and save interval parameters:
var interceptor = new DurableSessionInterceptor( store, Duration.ofHours(24), // session TTL Duration.ofMinutes(1) // cleanup interval);framework.interceptor(interceptor);The DurableSessionInterceptor in Detail
Section titled “The DurableSessionInterceptor in Detail”The interceptor runs at BEFORE_DEFAULT priority, so it executes before application interceptors.
On each inspect() call:
- It checks for an
X-Atmosphere-Session-Tokenheader (or query parameter). - If a token is found and the session exists in the store, it restores broadcaster subscriptions via
BroadcasterFactory.lookup()and room memberships viaRoomManager.room().join(). The session is updated with the new resource ID. - If no token is found (or the session expired), a new
DurableSessionis created with a UUID token and saved to the store. The token is returned in the response header. - In both cases, a disconnect listener is registered that captures the resource’s current rooms and broadcasters into the store when the connection closes.
The interceptor guards against double-save when both onDisconnect and onClose fire for the same resource by tracking resource UUIDs in a ConcurrentHashMap.newKeySet().
ConversationPersistence for AI Memory
Section titled “ConversationPersistence for AI Memory”Separate from session state, the ConversationPersistence SPI (in the atmosphere-ai module) persists AI conversation history so that users can resume conversations across server restarts:
public interface ConversationPersistence { Optional<String> load(String conversationId); void save(String conversationId, String data); void remove(String conversationId); default boolean isAvailable() { return true; }}Two implementations are provided:
| Implementation | Module |
|---|---|
SqliteConversationPersistence | durable-sessions-sqlite |
RedisConversationPersistence | durable-sessions-redis |
These share the same backend connections as the corresponding SessionStore implementations. The PersistentConversationMemory class handles serialization and sliding-window logic on top of the persistence SPI.
Companion: mid-stream reattach for @Prompt runs
Section titled “Companion: mid-stream reattach for @Prompt runs”Durable sessions restore room + broadcaster memberships after a
disconnect — the client sees the same presence and subscriptions it
had before. But an @Prompt that was actively streaming when the
client dropped needs a parallel mechanism: the buffered events the
client didn’t finish receiving.
That’s what RunRegistry + X-Atmosphere-Run-Id do. On every
@Prompt dispatch AiEndpointHandler registers a run with an
AgentResumeHandle and returns the run id as the
X-Atmosphere-Run-Id response header. RunEventCapturingSession
mirrors every session.send / complete / error into the run’s
bounded RunEventReplayBuffer. When the client reconnects carrying
the run id, RunReattachSupport.replayPendingRun drains the buffer
onto the new resource so the user catches up on the tokens they
missed — routed through the broadcaster’s filter chain so
PiiRedactionFilter applies identically to replay and live frames.
DurableSessionInterceptor stashes the header into the request
attribute org.atmosphere.session.runId so AiEndpointHandler.onReady
sees it without a compile-time dependency on the durable-sessions
module.
Ownership is enforced: replay refuses when the reconnecting caller’s
userId does not match the run’s registered userId, so a bearer
token leak cannot replay someone else’s conversation. Anonymous runs
keep the open-mode carve-out for demo deployments.
Combining with Clustering
Section titled “Combining with Clustering”Durable sessions and clustering serve different purposes and work well together:
- Clustering (Chapter 16) ensures messages reach all nodes during normal operation
- Durable sessions ensure clients can reconnect after a node failure or restart and resume their previous state
For a fully resilient deployment, combine a clustered broadcaster with RedisSessionStore so that session state is accessible from any node:
@Beanpublic SessionStore sessionStore() { return new RedisSessionStore("redis://localhost:6379");}Summary
Section titled “Summary”- The
SessionStoreSPI has three implementations:InMemorySessionStore(testing),SqliteSessionStore(single node), andRedisSessionStore(clustered) DurableSessionInterceptortransparently saves and restores room memberships and broadcaster subscriptions using theX-Atmosphere-Session-TokenheaderDurableSessionis a Java record capturing token, resource ID, rooms, broadcasters, metadata, and timestamps- Spring Boot auto-configures the interceptor when a
SessionStorebean is present ConversationPersistenceprovides a parallel SPI for persisting AI conversation history- No application code changes are needed — the interceptor works transparently with any
@ManagedService
Next up: Chapter 18: Observability covers Micrometer metrics, OpenTelemetry tracing, and health checks.