bug: HikariCP connection pool exhaustion causes backend outages #152

Closed
opened 2026-03-29 10:34:43 +02:00 by marcel · 1 comment
Owner

Problem

The backend repeatedly exhausts its HikariCP connection pool (total=10, active=10, idle=0) roughly 8–10 minutes after startup, causing all subsequent requests to fail with a 30-second timeout. A container restart temporarily fixes it, but it recurs.

HikariPool-1 - Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=2)

Root cause

spring.jpa.open-in-view is enabled (Spring Boot default).

This anti-pattern holds a database connection open for the entire duration of each HTTP request — from the moment the request arrives until the HTTP response is fully written. Under concurrent load (e.g. the dashboard's Promise.allSettled fires 3 API calls in parallel per page load), the pool of 10 connections fills up. Once full, new requests block for 30 seconds before failing.

This is confirmed in the startup logs:

WARN JpaBaseConfiguration$JpaWebConfiguration : 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

Impact

  • Affects production — not just development/testing. Any page that triggers multiple concurrent API calls (like the new dashboard) can exhaust the pool under modest load.
  • After exhaustion, the health check also fails (it needs a DB connection to report UP), making the container appear permanently unhealthy.

Fix

Add to backend/src/main/resources/application.yaml:

spring:
  jpa:
    open-in-view: false

This releases DB connections as soon as a @Transactional method returns, instead of holding them for the full request lifecycle. All existing service methods are already properly bounded with @Transactional, so no other code changes are needed.

Acceptance criteria

  • spring.jpa.open-in-view: false added to application.yaml
  • Backend stays healthy under sustained use (no pool exhaustion after 10+ minutes)
  • Confirm no lazy-loading LazyInitializationException appears in logs after the change (all needed associations should be eagerly fetched or loaded within transactions)
## Problem The backend repeatedly exhausts its HikariCP connection pool (`total=10, active=10, idle=0`) roughly 8–10 minutes after startup, causing all subsequent requests to fail with a 30-second timeout. A container restart temporarily fixes it, but it recurs. ``` HikariPool-1 - Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=2) ``` ## Root cause **`spring.jpa.open-in-view` is enabled (Spring Boot default).** This anti-pattern holds a database connection open for the *entire duration of each HTTP request* — from the moment the request arrives until the HTTP response is fully written. Under concurrent load (e.g. the dashboard's `Promise.allSettled` fires 3 API calls in parallel per page load), the pool of 10 connections fills up. Once full, new requests block for 30 seconds before failing. This is confirmed in the startup logs: ``` WARN JpaBaseConfiguration$JpaWebConfiguration : 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 ``` ## Impact - **Affects production** — not just development/testing. Any page that triggers multiple concurrent API calls (like the new dashboard) can exhaust the pool under modest load. - After exhaustion, the health check also fails (it needs a DB connection to report UP), making the container appear permanently unhealthy. ## Fix Add to `backend/src/main/resources/application.yaml`: ```yaml spring: jpa: open-in-view: false ``` This releases DB connections as soon as a `@Transactional` method returns, instead of holding them for the full request lifecycle. All existing service methods are already properly bounded with `@Transactional`, so no other code changes are needed. ## Acceptance criteria - [ ] `spring.jpa.open-in-view: false` added to `application.yaml` - [ ] Backend stays healthy under sustained use (no pool exhaustion after 10+ minutes) - [ ] Confirm no lazy-loading `LazyInitializationException` appears in logs after the change (all needed associations should be eagerly fetched or loaded within transactions)
marcel added the bugdevops labels 2026-03-29 10:34:56 +02:00
Author
Owner

Already fixed. spring.jpa.open-in-view: false is set in application.yaml with an explanatory comment.

Lazy-loading safety audit — confirmed all lazy relationships are safe with the current setting:

Field How protected
Person.nameAliases (@OneToMany, lazy) @JsonIgnore — never serialized
PersonNameAlias.person (@ManyToOne(LAZY)) @JsonIgnore — never serialized
Notification.recipient (@ManyToOne(LAZY)) Never returned as HTTP response; only .getId() used inside @Transactional
PasswordResetToken.user (@ManyToOne(LAZY)) Never returned as HTTP response; only accessed inside @Transactional
InviteToken.createdBy (@ManyToOne(LAZY)) Always wrapped in InviteListItemDTO before returning

Full test suite (1213 tests) passes with no LazyInitializationException.

Already fixed. `spring.jpa.open-in-view: false` is set in `application.yaml` with an explanatory comment. **Lazy-loading safety audit** — confirmed all lazy relationships are safe with the current setting: | Field | How protected | |---|---| | `Person.nameAliases` (`@OneToMany`, lazy) | `@JsonIgnore` — never serialized | | `PersonNameAlias.person` (`@ManyToOne(LAZY)`) | `@JsonIgnore` — never serialized | | `Notification.recipient` (`@ManyToOne(LAZY)`) | Never returned as HTTP response; only `.getId()` used inside `@Transactional` | | `PasswordResetToken.user` (`@ManyToOne(LAZY)`) | Never returned as HTTP response; only accessed inside `@Transactional` | | `InviteToken.createdBy` (`@ManyToOne(LAZY)`) | Always wrapped in `InviteListItemDTO` before returning | Full test suite (1213 tests) passes with no `LazyInitializationException`.
Sign in to join this conversation.
No Label bug devops
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#152