Spring Session Redis OOM: Root Cause and Permanent Fix
The Problem
A back-office application running Spring Session with Redis started throwing OutOfMemoryError: Java heap space on login page requests. A restart fixed it immediately — but the real question was why it happened and how to prevent it from coming back.
The stack trace pointed straight at the culprit:
RedisIndexedSessionRepository.findById()
→ JdkSerializationRedisSerializer.deserialize()
→ DeserializingConverter.convert()
→ OutOfMemoryError: Java heap space
To make things worse, the initial OOM triggered a cascade: Tomcat forwarded to the error page, which tried to load the same session again, causing the same OOM in a loop.
Root Cause
Oversized Session Payload
JdkSerializationRedisSerializer uses Java's built-in ObjectOutputStream to serialize session attributes into Redis. When someone stores a large object in the session — a massive list, file bytes, cached query results — the serialized payload in Redis can balloon to tens or hundreds of megabytes.
When that session gets loaded, the entire byte array gets pulled into the JVM heap for deserialization. If there isn't enough headroom, you get an OOM.
The Cascade Effect
OOM → Tomcat forwards to /error → SessionRepositoryFilter reloads the same session → same OOM. If your error page depends on the session in any way, there's no escape.
Quick Fix
If a specific session is the culprit, delete it directly from Redis:
# Check session size
redis-cli MEMORY USAGE "spring:session:sessions:<sessionId>"
# Remove it
redis-cli DEL "spring:session:sessions:<sessionId>"
redis-cli DEL "spring:session:sessions:expires:<sessionId>"
Or restart the service if you can afford the session loss.
Permanent Fixes
1. Minimize What You Store in Sessions
This is the highest-impact change. Store only identifiers (user ID, role) in the session. Everything else — search results, file data, cached objects — goes into a separate store (Redis cache, database), and you keep just the lookup key in the session.
// Don't do this
session.setAttribute("searchResults", hugeList);
// Do this instead
session.setAttribute("searchResultKey", cacheKey);
List<Result> results = cacheService.get(cacheKey);
2. Replace the Serializer
JdkSerializationRedisSerializer is the default, but it's a poor choice for production. The serialized bytes are large (they include full class metadata), opaque (binary, unreadable in Redis CLI), and fragile (serialVersionUID mismatches break deserialization after class changes).
Switch to GenericJackson2JsonRedisSerializer:
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(
om.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(om);
}
One caveat: changing the serializer means existing sessions can't be deserialized. You'll need to flush all sessions during deployment, or write a fallback wrapper that tries both serializers.
3. Monitor Session Sizes
Add a guard that warns you before sessions get dangerously large:
redis-cli --scan --pattern "spring:session:sessions:*" | \
xargs -I {} redis-cli MEMORY USAGE {} | sort -n -k1
You can also add an HttpSessionListener that logs a warning when any attribute exceeds a size threshold.
4. Decouple Error Pages from Sessions
Make your error controller work without touching the session. At minimum, ensure the error dispatch path doesn't trigger session lookups. This breaks the cascade loop even if an OOM occurs elsewhere.
Summary
| Fix | Impact | Effort | |---|---|---| | Trim session data | Eliminates root cause | Code audit needed | | Replace serializer | Smaller payloads + debuggability | Deployment planning | | Monitor session sizes | Early warning | Low | | Session-free error pages | Stops cascade failures | Low | | Increase heap | Temporary relief only | Low |
Bumping the heap is a band-aid. The real fix is auditing what goes into the session and keeping it lean.