Session Management in Microservices
When you log in to a website, you expect it to remember you on every page until you log out. That memory is called a session. In a single application, sessions are easy: the server keeps your login in its own memory. In a microservices system, a request may reach any of dozens of service instances, so "remembering the user" needs a different design.
This tutorial explains what sessions are, why they are harder in microservices, and the main ways to manage them: sticky sessions, a shared session store, stateless JWT tokens and gateway-managed sessions. Each approach comes with simple Spring Boot examples.
1. What Is a Session?
HTTP is stateless: every request stands alone, and the server does not automatically know that two requests came from the same person. A session adds that memory. It links a series of requests to one user, so the system knows who you are, what you are allowed to do, and things like what is in your cart.
Session management covers the whole lifecycle: creating the session at login, identifying the user on each request, storing session data, expiring it after inactivity, and ending it at logout.
2. How Sessions Work in a Monolith
In a traditional single application, the server stores session data in its own memory and sends the browser a cookie with a random session ID (in Java, usually called JSESSIONID). The browser sends the cookie back on every request, and the server looks up the session.
1. Browser --- POST /login (username, password) ---> Server
2. Server creates session {id: abc123, user: "asha"} in its memory
3. Server --- Set-Cookie: JSESSIONID=abc123 ---> Browser
4. Browser --- GET /orders (Cookie: JSESSIONID=abc123) ---> Server
5. Server finds session abc123 in memory: this is "asha"
This works well because there is only one server, and it always holds the session.
3. Why Sessions Are Hard in Microservices
- Many instances: each service runs several copies behind a load balancer. If your session lives in instance A's memory, a request routed to instance B does not know you.
- Many services: the User, Order and Payment services all need to know who you are, but they are separate applications.
- Instances come and go: auto-scaling and deployments start and stop instances constantly. Sessions stored in memory are lost when an instance stops.
- Scaling needs statelessness: microservices scale best when any instance can serve any request.
- Security across boundaries: user identity must be passed safely from service to service.
Request 1 (login) --> Load Balancer --> Order Service A (session stored here)
Request 2 (orders) --> Load Balancer --> Order Service B ("Who are you?")
4. Session Management Approaches
There are four common solutions. Each one answers the question "where does the session live?" differently:
- Sticky sessions: the load balancer always sends a user to the same instance.
- Shared session store: sessions live in a central store such as Redis that every instance can read.
- Stateless tokens (JWT): the client carries a signed token that contains the user's identity; servers store nothing.
- Gateway-managed sessions: the API Gateway keeps a session with the browser and passes tokens to the services behind it.
5. Approach 1: Sticky Sessions
With sticky sessions (also called session affinity), the load balancer remembers which instance served a user, usually through a cookie, and sends all of that user's requests to the same instance. The session can stay in that instance's memory.
- Pros: no code changes; easy way to move an existing application behind a load balancer.
- Cons: if the instance crashes or is redeployed, its users are logged out; load can become uneven; scaling down is harder; and it only works within one service, not across services.
Sticky sessions are a short-term fix, not a good long-term design for microservices.
6. Approach 2: Shared Session Store (Spring Session + Redis)
Instead of keeping sessions in each instance's memory, all instances store them in a shared, fast store such as Redis. The browser still uses a session cookie, but any instance can look the session up. With Spring Session, this needs almost no code changes.
Browser (Cookie: SESSION=abc123)
|
v
[ Load Balancer ] ---> Instance A --+
---> Instance B --+--> [ Redis ]
---> Instance C --+ session abc123 = {user: "asha", cart: [...]}
Step 1: Add the Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
Step 2: Configure Redis and Session Timeout
spring:
data:
redis:
host: localhost
port: 6379
session:
timeout: 30m # log out after 30 minutes of inactivity
redis:
namespace: shop:session # prefix for session keys in Redis
server:
servlet:
session:
cookie:
http-only: true # JavaScript cannot read the cookie
secure: true # only sent over HTTPS
same-site: lax # basic protection against cross-site requests
Step 3: Use the Session as Usual
package com.javabytechie.shop.cart;
import jakarta.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/cart")
public class CartController {
private static final String CART_KEY = "cart";
@PostMapping("/items")
public List<String> addItem(@RequestBody String productId, HttpSession session) {
List<String> cart = getCart(session);
cart.add(productId);
session.setAttribute(CART_KEY, cart); // saved to Redis, visible to every instance
return cart;
}
@GetMapping
public List<String> viewCart(HttpSession session) {
return getCart(session);
}
@SuppressWarnings("unchecked")
private List<String> getCart(HttpSession session) {
Object cart = session.getAttribute(CART_KEY);
return cart != null ? (List<String>) cart : new ArrayList<>();
}
}
The code uses the normal HttpSession API. Spring Session quietly stores it in Redis, so it survives restarts and works across instances.
- Pros: easy to adopt; sessions can be revoked instantly by deleting them; works with existing session-based code.
- Cons: every request needs a Redis lookup; Redis must be highly available, or nobody can log in; every service that reads the session needs access to Redis.
7. Approach 3: Stateless JWT Tokens
A JSON Web Token (JWT) is a compact, digitally signed token that contains information about the user, such as their ID and roles. After login, the authentication service gives the client a JWT. The client sends it in the Authorization header on every request, and each service checks the signature to trust it. No server needs to store the session. See What Is JWT for a deeper look.
header.payload.signature
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "asha", "roles": ["CUSTOMER"], "iat": 1767225600, "exp": 1767226500}
Signature: created with a secret or private key; any change to the token breaks it
1. Client --- POST /auth/login ---> Auth Service
2. Auth Service checks the password and returns a signed JWT (valid for 15 minutes)
3. Client --- GET /orders (Authorization: Bearer <jwt>) ---> Order Service
4. Order Service verifies the signature and expiry, then reads the user from the token
(no database or session lookup needed)
Issuing and Validating a JWT (Auth Service)
This example uses the open-source JJWT library (version 0.12 or later):
package com.javabytechie.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class JwtService {
private static final Duration ACCESS_TOKEN_LIFETIME = Duration.ofMinutes(15);
private final SecretKey signingKey;
// The secret must be at least 32 characters for HS256 and must come from configuration,
// never from source code.
public JwtService(@Value("${security.jwt.secret}") String secret) {
this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String username, List<String> roles) {
Instant now = Instant.now();
return Jwts.builder()
.id(UUID.randomUUID().toString()) // unique token ID (jti) for logout
.subject(username)
.claim("roles", roles)
.issuer("javabytechie-auth")
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(ACCESS_TOKEN_LIFETIME)))
.signWith(signingKey)
.compact();
}
/** Returns the claims if valid; throws JwtException if forged or expired. */
public Claims validate(String token) throws JwtException {
return Jwts.parser()
.verifyWith(signingKey)
.requireIssuer("javabytechie-auth")
.build()
.parseSignedClaims(token)
.getPayload();
}
}
Making Other Services Stateless (Spring Security)
Downstream services do not need to create tokens; they only verify them. Spring Security's resource server support does this with a few lines:
package com.javabytechie.orders.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// No server-side session: every request must carry its own token.
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// CSRF protection is for cookie sessions; bearer tokens are not auto-sent.
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
// Validate the JWT in the Authorization header on every request.
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
# Public keys are downloaded from the identity provider to verify token signatures.
issuer-uri: https://auth.example.com/realms/shop
This setup expects tokens signed by an identity provider such as Keycloak or another OAuth 2.0 server, which publishes its public keys. Services then trust tokens without ever sharing a secret.
- Pros: no session storage; any instance of any service can verify the token; scales very well.
- Cons: a token stays valid until it expires, so logout and revocation need extra work; tokens grow if you add many claims; payloads are only encoded, not encrypted.
8. Refresh Tokens and Logout
Because a JWT cannot be "taken back", systems combine two tokens:
- Access token: short-lived (for example 5–15 minutes), sent with every API request.
- Refresh token: long-lived (hours or days), stored securely and used only to get a new access token. It is tracked on the server, so it can be revoked.
To log a user out immediately, add the access token's unique ID (jti) to a blocklist until the token would have expired anyway:
package com.javabytechie.auth;
import java.time.Duration;
import java.time.Instant;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class TokenBlocklist {
private static final String PREFIX = "blocked-token:";
private final StringRedisTemplate redis;
public TokenBlocklist(StringRedisTemplate redis) {
this.redis = redis;
}
/** Called on logout. The entry deletes itself when the token would have expired. */
public void block(String tokenId, Instant tokenExpiresAt) {
Duration remaining = Duration.between(Instant.now(), tokenExpiresAt);
if (!remaining.isNegative()) {
redis.opsForValue().set(PREFIX + tokenId, "logged-out", remaining);
}
}
/** Checked on each request, after the signature is verified. */
public boolean isBlocked(String tokenId) {
return Boolean.TRUE.equals(redis.hasKey(PREFIX + tokenId));
}
}
The blocklist only holds tokens that were logged out early, so it stays small. Short access-token lifetimes keep it smaller still.
9. Approach 4: Gateway-Managed Sessions
For browser applications, a popular design combines the best of both worlds. The browser keeps a simple, secure session cookie with the API Gateway (or a "Backend for Frontend"). The gateway stores the user's tokens on the server side and forwards an access token to the microservices on each request.
Browser
| HttpOnly session cookie
v
[ API Gateway ] ----> keeps tokens in its session store (e.g. Redis)
| and refreshes access tokens automatically
| Authorization: Bearer <access token>
v
Microservices (Order, Payment, ...)
- Pros: tokens never reach the browser, so JavaScript cannot steal them; services stay stateless; logout happens in one place.
- Cons: the gateway becomes stateful and must be highly available; adds one more component to run.
Spring Cloud Gateway supports this with Spring Security's OAuth 2.0 login and its TokenRelay filter, which forwards the user's access token to downstream services.
10. Passing the User Between Services
When the Order Service calls the Payment Service on behalf of a user, the Payment Service also needs to know who the user is. The simplest approach is to forward the same access token:
package com.javabytechie.orders.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.client.RestClient;
@Configuration
public class PaymentClientConfig {
@Bean
public RestClient paymentClient(RestClient.Builder builder) {
return builder
.baseUrl("http://payment-service")
.requestInterceptor((request, body, execution) -> {
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof JwtAuthenticationToken jwtAuth) {
// Forward the caller's token so Payment Service sees the same user.
String token = jwtAuth.getToken().getTokenValue();
request.getHeaders().setBearerAuth(token);
}
return execution.execute(request, body);
})
.build();
}
}
Never pass the user's identity as a plain header such as X-User-Id that any caller could fake, unless the network is locked down so only the gateway can set it.
11. Comparing the Approaches
| Aspect | Sticky Sessions | Shared Store (Redis) | Stateless JWT | Gateway-Managed |
|---|---|---|---|---|
| Where state lives | One app instance | Central store | Inside the token | Gateway session store |
| Survives instance crash | No | Yes | Yes | Yes |
| Scalability | Limited | Good | Excellent | Good |
| Instant logout | Yes | Yes | Needs blocklist | Yes |
| Works across services | No | If all share the store | Yes | Yes |
| Extra infrastructure | Load balancer setting | Highly available Redis | Identity provider | Gateway + session store |
| Best for | Quick fix for legacy apps | Session-heavy web apps | APIs, mobile apps, service-to-service | Browser apps (SPAs) |
12. Example: An Online Store Login
Here is how a typical online store combines these ideas:
- Login: the customer signs in through the identity provider (for example Keycloak). The API Gateway receives an access token (valid 10 minutes) and a refresh token, stores them in Redis, and gives the browser an
HttpOnly,Securesession cookie. - Browsing: each request from the browser carries the cookie. The gateway finds the session, adds
Authorization: Bearer <access token>and forwards the request. - Placing an order: the Order Service validates the JWT, reads the customer ID from it, and forwards the same token when calling the Payment Service.
- Token refresh: when the access token expires, the gateway uses the refresh token to get a new one. The customer notices nothing.
- Logout: the gateway deletes the session from Redis and revokes the refresh token. Any copy of the old access token expires within minutes.
The result: services stay stateless and easy to scale, tokens never reach browser JavaScript, and logout works reliably.
13. Security Best Practices
- Always use HTTPS so session cookies and tokens cannot be read in transit.
- Mark session cookies
HttpOnly,SecureandSameSite. - Keep access tokens short-lived (minutes) and use refresh tokens for longer sessions.
- Rotate refresh tokens on every use and revoke them on logout or suspicious activity.
- Validate the signature, expiry, issuer and audience of every JWT.
- Never put passwords or sensitive personal data in a JWT; its payload is readable by anyone.
- Prefer asymmetric signing (RS256, ES256) so services only need the public key.
- Avoid storing tokens in browser
localStorage, where injected scripts can read them. - Use CSRF protection whenever authentication relies on cookies.
- Expire inactive sessions and give users a way to log out of all devices.
For the basics of identity and permissions, see Authentication and Authorization.
14. Common Mistakes
- Keeping sessions in instance memory and wondering why users are randomly logged out after scaling or deployment.
- Long-lived JWTs (days or weeks) with no way to revoke them.
- Trusting a token without checking its signature, or accepting tokens with the
nonealgorithm. - Storing large objects in the session, which makes every request slower.
- Hard-coding signing secrets in source code instead of a secrets manager.
- A single, non-replicated Redis for sessions, which becomes a single point of failure.
15. Conclusion
Session management in microservices means remembering users without tying them to a single server. Sticky sessions are a quick but fragile fix. A shared session store such as Redis with Spring Session keeps familiar session code and works across instances. Stateless JWTs let every service verify users on its own and scale best, combined with short lifetimes, refresh tokens and a blocklist for logout. For browser applications, a gateway-managed session keeps tokens safely on the server while services stay stateless. Choose the approach that fits your clients and security needs, protect tokens and cookies carefully, and your users will stay securely logged in, no matter which instance serves them.
Frequently Asked Questions
1. What is session management in microservices?
It is the way a microservices system remembers who a user is across many requests, services and server instances: creating a session or token at login, recognising the user on every request, expiring it, and ending it at logout.
2. Why can't microservices use normal in-memory sessions?
Each service runs several instances behind a load balancer, and instances start and stop often. A session stored in one instance's memory is unknown to the others and lost when that instance stops, so users would be logged out unexpectedly.
3. What is the difference between stateful and stateless sessions?
In a stateful design, the server stores session data and the client holds only an ID. In a stateless design, the client holds a signed token with the needed information, and servers store nothing; they just verify the token.
4. What are sticky sessions?
Sticky sessions make the load balancer send all requests from one user to the same server instance. They are easy to set up, but users lose their session if that instance fails, and load can become unbalanced.
5. What is Spring Session?
Spring Session is a Spring project that stores HttpSession data in an external store such as Redis or a database instead of server memory. Existing session code keeps working, but sessions are shared across all instances.
6. Why is Redis popular for storing sessions?
Redis keeps data in memory, so reads and writes are very fast. It supports automatic expiry (TTL), replication for high availability, and is easy to share between many application instances.
7. What is a JWT?
A JSON Web Token is a compact token made of a header, a payload with claims (such as user ID, roles and expiry) and a signature. Services verify the signature to trust the token without contacting a central session store.
8. Is a JWT encrypted?
Usually not. A normal signed JWT is only Base64URL-encoded, so anyone can read its payload. The signature only proves it has not been changed. Never put secrets or sensitive personal data in a JWT.
9. How do you log out a user when using JWTs?
Keep access tokens short-lived, revoke the refresh token on logout, and, if immediate logout is required, store the token's unique ID (jti) in a blocklist until it expires:
redis.opsForValue().set("blocked-token:" + tokenId, "logged-out", remainingLifetime);
10. What is the difference between an access token and a refresh token?
An access token is short-lived and sent with every API request. A refresh token lives longer, is kept more securely, and is used only to obtain new access tokens. Refresh tokens are tracked by the server so they can be revoked.
11. How long should an access token be valid?
Typically 5 to 15 minutes. Short lifetimes limit the damage if a token is stolen, while refresh tokens keep users logged in without asking them to sign in again.
12. Where should a browser app store tokens?
The safest option is not to store them in the browser at all: let a gateway or backend keep tokens and give the browser an HttpOnly, Secure cookie. Avoid localStorage, because any injected script can read it.
13. How does the API Gateway help with session management?
The gateway can check authentication once at the entry point, keep the browser session, refresh tokens, and forward a valid access token to each service. Services then only verify tokens and stay stateless.
14. How do microservices pass user identity to each other?
The most common way is to forward the user's access token in the Authorization: Bearer header. Each service verifies it independently. For calls not made on behalf of a user, services use their own client-credentials tokens.
15. What does SessionCreationPolicy.STATELESS do in Spring Security?
It tells Spring Security never to create or use an HttpSession to store the security context. Each request must authenticate itself, usually with a bearer token, which is ideal for stateless microservices.
16. Do I need CSRF protection in microservices?
You need it when authentication relies on cookies that browsers send automatically, such as a gateway session cookie. Pure bearer-token APIs are not vulnerable to CSRF in the same way, because the browser does not attach the token automatically.
17. What is the difference between HS256 and RS256?
HS256 signs and verifies with one shared secret, so every verifying service must hold that secret. RS256 signs with a private key and verifies with a public key, so services can verify tokens without being able to create them, which is safer for microservices.
18. What is session timeout?
It is the period of inactivity after which a session expires and the user must log in again, for example 30 minutes. It limits the risk of an unattended, logged-in device being misused.
19. Can I combine JWTs and server-side sessions?
Yes, and many systems do. A common pattern keeps a server-side session between the browser and the gateway, and uses JWTs between the gateway and the microservices.
20. Which session management approach should I choose?
For APIs, mobile apps and service-to-service calls, use stateless JWTs with short lifetimes and refresh tokens. For browser apps, prefer gateway-managed sessions with secure cookies. Use a shared Redis session store when migrating existing session-based applications, and treat sticky sessions only as a temporary fix.