Disabling Open Session in View to Release Connections Early
This guide is part of ORM Connection Lifecycle Hooks. Spring Boot enables open session in view by default, and it logs a warning most teams have learned to ignore:
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
The warning is about lazy loading. The operational consequence is about connections: with open session in view, the persistence context — and, in Hibernate’s default connection handling, the JDBC connection it borrowed — stays attached to the request until the response has been rendered and written. A request that spends 20 ms in the database and 180 ms serialising JSON and waiting on the client holds a pooled connection for all 200 ms. The pool’s capacity is then set by response time rather than by query time, and a slow client can occupy a database connection.
The objective is to release connections when the database work ends, without breaking the lazy-loading the application currently relies on.
Rapid incident diagnosis
Confirm the setting. It is on unless explicitly disabled:
curl -s localhost:8080/actuator/env | jq -r '.propertySources[].properties["spring.jpa.open-in-view"].value' | head -1
grep -rn "open-in-view" src/main/resources/
Compare hold time with query time. If the pool’s mean hold time far exceeds the database’s mean statement time, something holds connections beyond their work:
# mean hold: seconds of usage per borrow
rate(hikaricp_connections_usage_seconds_sum[5m]) / rate(hikaricp_connections_usage_seconds_count[5m])
SELECT round(sum(mean_exec_time * calls) / sum(calls)::numeric, 2) AS mean_statement_ms
FROM pg_stat_statements WHERE userid = (SELECT oid FROM pg_roles WHERE rolname = 'orders_app');
A 200 ms mean hold against a 4 ms mean statement time is the signature.
Check what the connections are doing on the server. With open session in view, many sessions sit idle while their requests render:
SELECT state, count(*), max(now() - state_change) AS longest
FROM pg_stat_activity WHERE application_name = 'orders-api' GROUP BY state;
Look at Hibernate’s connection handling mode. Since Hibernate 5, the default for JPA transactions is delayed acquisition and release after the transaction — but with open session in view, a session remains open outside any transaction, and any lazy load outside the transaction acquires a connection again and holds it until the session closes:
logging.level.org.hibernate.resource.jdbc=DEBUG
The log shows acquisition and release events; a release that happens only at request end confirms the behaviour.
Mathematical sizing or parameter formula
Pool demand is arrival rate times hold time, so hold time is the multiplier:
in_flight = requests_per_second × hold_seconds
At 300 requests per second with a 200 ms hold, 300 × 0.2 = 60 connections are busy; with a 24 ms hold, 300 × 0.024 = 7.2. The same traffic needs a pool eight times smaller.
Slow clients make it worse. When the response is written directly to the socket, a slow client extends the hold:
hold_seconds ≈ query_time + render_time + max(0, client_write_time)
A mobile client on a poor connection can take seconds to receive a large response. With open session in view, that time is spent holding a database connection; without it, the connection was returned long before.
Fleet effect. Because each replica has its own pool:
fleet_connections = replicas × ceil( requests_per_second_per_replica × hold_seconds ÷ 0.7 )
Twelve replicas at 25 requests per second each: with the long hold, ceil(25 × 0.2 ÷ 0.7) = 8 per replica, 96 connections. With the short hold, 1 per replica — which is too small for burst tolerance, so a floor of 4 or 5 would be used, still less than half the original.
The saving is capacity, not latency. Disabling open session in view does not make requests faster; it makes the pool able to serve far more of them concurrently with the same number of connections.
Exact remediation & configuration
Disable it, then fix what breaks:
spring:
jpa:
open-in-view: false
properties:
hibernate:
connection:
handling_mode: DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
The handling mode is the default for JPA transactions, but stating it makes the intent explicit: acquire the connection when the first statement runs, release it at the transaction boundary rather than when the session closes.
Load what the response needs inside the transaction. The errors that follow are LazyInitializationException, and each one names a relation that was loaded during rendering:
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.Order#88] - no Session
Three fixes, in order of preference:
// 1. Fetch what the response needs, explicitly, in the query
@Query("select o from Order o join fetch o.items join fetch o.customer where o.id = :id")
Optional<Order> findForDetailView(@Param("id") Long id);
// 2. Or use an entity graph
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findWithItemsById(Long id);
// 3. Or map to a DTO inside the transaction, so the view never touches entities
@Transactional(readOnly = true)
public OrderView load(Long id) {
Order o = repo.findForDetailView(id).orElseThrow();
return new OrderView(o.getId(), o.getCustomer().getName(),
o.getItems().stream().map(ItemView::of).toList());
}
The third is the one that keeps working as the code evolves: the controller receives a fully materialised view object, and no entity proxy can escape the transaction.
Keep read-only transactions read-only. @Transactional(readOnly = true) lets Hibernate skip dirty checking and, on some databases, routes to a replica through a routing data source:
@Transactional(readOnly = true)
public Page<OrderSummary> search(SearchCriteria c, Pageable p) { ... }
Watch for N+1 queries appearing. Eager fetching in a query is one round trip; a loop that touches a lazy collection per row is many. Enabling Hibernate’s statistics in staging makes the count visible:
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
Roll out behind a flag if the codebase is large. spring.jpa.open-in-view can be set per environment, so the change can be validated in staging and canaried in production without a code change.
Migrating a large codebase safely
Turning the setting off in a mature application surfaces every place that relied on it, which can be hundreds of endpoints. A staged approach keeps the change reviewable.
Stage one: measure the prize. Record the mean hold time and the pool’s peak utilisation before any change. If hold time already tracks query time — because the application returns DTOs everywhere — the setting is doing nothing and can be turned off immediately.
Stage two: find the lazy loads. Run the integration test suite with the setting off and collect every LazyInitializationException. Each one is a specific relation on a specific endpoint, which makes the work list concrete and finite:
mvn test -Dspring.jpa.open-in-view=false 2>&1 | grep -A2 LazyInitializationException | sort -u
For endpoints without test coverage, the same exceptions appear in staging; a canary with the setting off catches them before production.
Stage three: fix by endpoint, not globally. Converting one controller at a time to DTO projections keeps each change small. Endpoints that are already DTO-based need no work at all.
Stage four: resize the pool. After the change, hold time drops and the pool’s utilisation falls with it. Resizing releases connections back to the database budget, which is usually the point of the exercise:
spring:
datasource:
hikari:
maximum-pool-size: 8 # was 24, when hold time was eight times longer
Keep some headroom rather than sizing exactly to the new average — bursts still need connections, and the pool is no longer masking them with a long queue.
| Stage | Output | Risk |
|---|---|---|
| Measure hold versus query time | The size of the prize | None |
| Run tests with the flag off | A finite list of endpoints | None |
| Fix endpoints to DTO projections | Connections released earlier | N+1 queries if fetching is careless |
| Resize the pool | Budget returned to the database | Under-sizing if bursts were not considered |
Validation & verification
Hold time falls to query time. After the change, the pool’s mean hold should be close to the database’s mean statement time plus a small margin:
rate(hikaricp_connections_usage_seconds_sum[5m]) / rate(hikaricp_connections_usage_seconds_count[5m])
Idle sessions disappear. On the database, sessions from the service should be active when busy and absent otherwise, rather than idle for the duration of requests.
No lazy-loading errors in production. Search logs for LazyInitializationException over a full day after the canary. Any occurrence names an endpoint that still needs a projection.
Query count per request did not explode. Compare Hibernate’s statistics before and after on a representative endpoint; the number of statements per request should be the same or lower, never higher. A rise means a fetch that used to happen lazily once is now happening per row.
Slow clients no longer hold connections. Simulate a slow consumer — curl --limit-rate 10k against an endpoint returning a large payload — and confirm the database session count does not rise for the duration of the download.
Frequently Asked Questions
Does disabling it break Spring Data REST or similar?
What about hibernate.enable_lazy_load_no_trans?
Is this the same problem in other frameworks?
CONN_MAX_AGE keeps connections but not transactions, so it is different; Rails holds a connection for the whole request unless it is released explicitly, which is the same shape and is covered in Sizing ActiveRecord Pools for Sidekiq Workers.Should the pool shrink immediately after the change?
Related
- ORM Connection Lifecycle Hooks — the parent guide to checkout and check-in events.
- Spring Boot DataSource Configuration — where the pool this page resizes is configured.
- Detecting ORM Connection Leaks in Production — finding connections held even longer than a request.
- Applying Little’s Law to Connection Pool Sizing — why hold time is the multiplier.