Running Spring Boot applications on Google Cloud Run offers fantastic benefits: auto-scaling, pay-per-use, and simplified deployments. It’s a great platform for microservices. However, serverless environments are less forgiving of memory inefficiencies than traditional servers. Recently, we encountered a persistent memory leak in a Spring Boot application running on Cloud Run, leading to dreaded `OutOfMemoryError` (OOM) exceptions and container terminations. The culprit? A default Spring Boot setting that’s convenient but potentially dangerous: `spring.jpa.open-in-view`.
The Mystery: Creeping Memory Usage and Container Deaths
Our application, using Spring Boot with JPA/Hibernate to interact with a Cloud SQL database, started exhibiting a slow but steady increase in memory usage under load. Cloud Run metrics clearly showed the memory climbing over time, eventually hitting the container’s limit, at which point the instance would be terminated. Debugging was tricky, as heap dumps didn’t immediately pinpoint a single massive object, suggesting a death by a thousand cuts — many objects lingering longer than they should.
The breakthrough came from a warning message in the logs that many might overlook:
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning.
While just a warning, this highlighted that the “Open Session in View” pattern was active.
What is `spring.jpa.open-in-view`?
The `spring.jpa.open-in-view` property, when set to `true` (the default in Spring Boot for web applications), keeps the JPA `EntityManager` (and the underlying Hibernate Session) open for the entire duration of a web request. The session is typically opened when the request hits the server and is only closed *after* the view has been rendered and the response is sent back to the client.
The benefit of this approach is convenience. It allows lazy-loading of entity associations within the view layer (e.g., in Thymeleaf templates or during JSON serialization) without throwing the infamous `LazyInitializationException`.
The dark side of this convenience includes:
1. Prolonged Database Connections: The Hibernate Session holds the database connection open for the entire request lifecycle. This can exhaust connection pools under load.
2. Bloated Session State: Any entities loaded into the Session remain in memory. Across the lifetime of a long request, or one that loads many entities, this can consume significant heap space.
3. Uncontrolled Transactions: Database operations can potentially occur outside of an explicit `@Transactional` service layer method, making transaction management less clear.
4. Memory Leaks: Entities remain attached to the Session. If these Sessions are not garbage collected promptly after the request, the memory footprint grows.
Why this pattern is deadly on Cloud Run
In a resource-constrained environment like Cloud Run:
a) Fixed Memory Limits: Each container instance has a hard memory limit. Unlike a VM, it can’t just expand. Persistent sessions holding onto large object graphs will quickly exhaust this limit.
b) Instance Reuse & Concurrency: Cloud Run reuses instances for multiple requests. If memory isn’t fully reclaimed after each request, subsequent requests on the same instance suffer from reduced available memory, accelerating the journey to an OOM.
c) Garbage Collection Pressure: While the JVM has garbage collection, holding entities in an open Hibernate Session prevents them from being garbage collected, even if they are no longer strictly needed after the service layer completes.
The “Open Session in View” pattern turns the Hibernate Session into a potential time bomb for memory usage in a serverless function.
The Fix: Disabling Open Session in View
The solution was simple to implement. We disabled the pattern by adding the following line to `application.properties`:
properties
spring.jpa.open-in-view=false
Or, if you use YAML configuration (application.yml):
yaml
spring:
jpa:
open-in-view: false
Upon deploying this change, the memory leak vanished. Memory usage on Cloud Run instances stabilized, even under load, and the OOM errors ceased.
The Catch: Handling `LazyInitializationException`
Disabling `spring.jpa.open-in-view=false` means the Hibernate Session is typically flushed and closed when the `@Transactional` service method returns. Any attempt to access a lazily-loaded association *outside* of the transactional context (e.g., in the controller after the service call, or during JSON serialization) will now result in a `LazyInitializationException`.
This is a good thing! It forces you to be explicit about data fetching. Here are common strategies to manage this:
a. DTOs (Data Transfer Objects): Map your JPA entities to DTOs *within* the service layer. The DTOs should contain only the data needed by the view or API client. This is often the cleanest approach.
b. JPQL/HQL JOIN FETCH: Explicitly fetch required associations within your queries:
jpql
SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id
c. EntityGraphs: Programmatically define which parts of the entity graph should be loaded:
java
EntityGraph graph = entityManager.getEntityGraph(“userWithRoles”);
Map
hints = new HashMap<>(); hints.put(“javax.persistence.fetchgraph”, graph);
User user = entityManager.find(User.class, id, hints);
d. Eager Fetching (`FetchType.EAGER`): While an option on the entity mapping, use this sparingly, as it can lead to fetching too much data (N+1 select problems or large joins).
e. Re-opening Transactions: In rare cases, you might need to open a new read-only transaction to fetch additional data, but this should be an exception.
The DTO pattern combined with `JOIN FETCH` or EntityGraphs for specific use cases is generally the most robust solution.
Recommendations for Spring Boot on Cloud Run
Default to `spring.jpa.open-in-view=false For applications running on Cloud Run or other memory-constrained serverless environments, make this your default setting.
Test Rigorously: Before pushing to production, thoroughly test your application in a lower environment. Pay close attention to areas where entities are accessed after service calls. Implement integration tests that cover API responses to ensure all required data is present.
Load Test: Simulate production load to confirm that the memory issues are resolved and that you haven’t introduced a performance bottleneck with your data fetching strategies.
Monitor: Keep an eye on your Cloud Run memory metrics and logs.
Conclusion
The convenience of `spring.jpa.open-in-view=true` hides a significant risk of memory leaks and OOM errors, especially in serverless environments like Cloud Run. By disabling this default and adopting explicit data fetching strategies, you can build more resilient, performant, and memory-efficient Spring Boot applications in the cloud. Don’t let the default settings lead to production outages!
Disclaimer: Always test configuration changes thoroughly in your non-production environments before applying them to production. The best data fetching strategy depends on your specific application’s needs.
Before your next deployment, check your application.properties file. That one line change could save you from a world of memory-related pain. Have you been bitten by this default before? Share your experiences in the comments below!
Source Credit: https://medium.com/google-cloud/slaying-the-silent-memory-hog-how-spring-jpa-open-in-view-b7aab9ae88dc?source=rss—-e52cf94d98af—4
