Skip to content

perf(docker): pin JVM container memory limits to stop host oversubscription - #1410

Merged
FelixTJDietrich merged 1 commit into
mainfrom
perf/jvm-memory-limits
Jul 21, 2026
Merged

perf(docker): pin JVM container memory limits to stop host oversubscription#1410
FelixTJDietrich merged 1 commit into
mainfrom
perf/jvm-memory-limits

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Staging's application server was intermittently very slow and its host was swapping. Live profiling on the staging box traced it to a memory-management root cause, not a "needs more RAM" problem:

  • No container had a memory limit (mem_limit=0 on every JVM service). With no cgroup limit, the Paketo memory calculator sizes -Xmx against the whole host.
  • Measured heap ceilings on the 11 GiB staging host:
    • webhook-server: -Xmx 8.4 GiB — but a full-GC class histogram showed a live set of ~150 MiB. Pure waste.
    • application-server: -Xmx 5.36 GiB.
  • 8.4 + 5.36 = 13.7 GiB of heap ceiling on 11 GiB of RAM → the box swaps → the app JVM sits in near-constant G1 Concurrent GC (3 cores pegged) walking swapped pages → slow, unresponsive API.

Capping webhook-server live (to 1.5 GiB) during the investigation immediately dropped host memory use from 9.0 → 6.3 GiB, drained swap, and relieved the app — confirming oversubscription as the cause.

Change

Add an explicit, per-service, environment-overridable mem_limit to the three JVM containers so each sizes its heap for its own container instead of the host:

Service Default limit Override
webhook-server 1536m WEBHOOK_SERVER_MEM_LIMIT
application-server 6g APPLICATION_SERVER_MEM_LIMIT
application-worker 4g APPLICATION_WORKER_MEM_LIMIT

Defaults keep the sum comfortably under an 11 GiB host with room for Postgres/NATS. Larger hosts can raise them.

Follow-up (not in this PR)

Profiling also found the app-server holds ~1.8 GiB of heap as buffered NATS JetStream messages — the github stream has 2,000,000 messages / 30 GiB across 94 consumers, and maxAckPending (500) is a per-consumer cap with no aggregate bound, so in-flight memory scales with consumer count. Worth a dedicated PR to bound total in-flight (and it needs load testing), so it's intentionally out of scope here.

Testing

  • Compose files validated with docker compose config.
  • Behaviour verified live on staging: capping the webhook heap stopped the swapping and restored app responsiveness.
  • No application code changed; Java/TS quality gates are N/A (docker/ + changeset only).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance / Operations
    • Added explicit container memory limits for the application-server, application-worker, and webhook-server to ensure JVM heap sizing follows the container boundary.
    • Helps prevent host memory oversubscription and swap thrashing in co-located deployments.
    • Introduced configurable memory limit overrides:
      • webhook-server (default 1 GB via WEBHOOK_SERVER_MEM_LIMIT)
      • application-server (default 5 GB via APPLICATION_SERVER_MEM_LIMIT)
      • application-worker (default 3 GB via APPLICATION_WORKER_MEM_LIMIT)

@FelixTJDietrich
FelixTJDietrich requested a review from a team as a code owner July 20, 2026 15:52
@github-actions github-actions Bot added performance Speed, memory, or efficiency improvements ci GitHub Actions, workflows, build pipeline changes infrastructure Docker, containers, and deployment infrastructure size:S labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54287624-a92c-477e-9765-714deb45b665

📥 Commits

Reviewing files that changed from the base of the PR and between 81d3f36 and 7e218fe.

📒 Files selected for processing (3)
  • .changeset/jvm-memory-limits.md
  • docker/compose.app.yaml
  • docker/compose.core.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • docker/compose.core.yaml
  • .changeset/jvm-memory-limits.md

📝 Walkthrough

Walkthrough

Compose now assigns configurable memory limits to the application-server, application-worker, and webhook-server containers. The changeset documents defaults, environment overrides, and host-memory sizing guidance.

Changes

JVM container memory limits

Layer / File(s) Summary
Configure service memory limits
docker/compose.app.yaml, docker/compose.core.yaml, .changeset/jvm-memory-limits.md
The three JVM services receive configurable memory limits with documented defaults and environment-variable overrides.

Estimated code review effort: 2 (Simple) | ~5 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding container memory limits to prevent JVM heap oversubscription.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/jvm-memory-limits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker/compose.app.yaml`:
- Around line 96-100: The memory budget must include worker sandbox concurrency.
In docker/compose.app.yaml at lines 96-100 and 299-300, cap
SANDBOX_MAX_CONCURRENT or lower the JVM memory limits so the combined allocation
remains within the host budget; retain the webhook memory limit in
docker/compose.core.yaml lines 11-16. Update .changeset/jvm-memory-limits.md
lines 12-16 with the sandbox ceiling and minimum recommended host RAM.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9e15a97-01f1-475c-9ea7-4405bbfb4297

📥 Commits

Reviewing files that changed from the base of the PR and between 8e3618c and 81d3f36.

📒 Files selected for processing (3)
  • .changeset/jvm-memory-limits.md
  • docker/compose.app.yaml
  • docker/compose.core.yaml

Comment thread docker/compose.app.yaml Outdated
Comment on lines +96 to +100
# Pin the container memory limit so the Paketo memory calculator sizes -Xmx for THIS container
# instead of the whole host. Unpinned, co-located JVMs each size their heap to the full host and
# their sum oversubscribes RAM (→ swap → GC thrash). This role hosts the NATS sync consumers, so
# it legitimately needs the most headroom — size it to the box, leaving room for webhook + db.
mem_limit: ${APPLICATION_SERVER_MEM_LIMIT:-6g}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== docker/compose.app.yaml (relevant ranges) ==\n'
sed -n '80,115p;285,310p' docker/compose.app.yaml

printf '\n== docker/compose.core.yaml ==\n'
sed -n '1,35p' docker/compose.core.yaml

printf '\n== .changeset/jvm-memory-limits.md ==\n'
sed -n '1,80p' .changeset/jvm-memory-limits.md

printf '\n== Search for sandbox/container memory settings and host sizing guidance ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' -e 'sandbox.*mem|mem.*sandbox|memory.*sandbox|APPLICATION_WORKER_MEM_LIMIT|WEBHOOK_SERVER_MEM_LIMIT|APPLICATION_SERVER_MEM_LIMIT|comfortably under host|leave room|host RAM|physical RAM|5 GiB|4 GiB|sandbox container' .

Repository: ls1intum/Hephaestus

Length of output: 10907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== sandbox concurrency / memory config ==\n'
sed -n '1,120p' server/src/main/java/de/tum/cit/aet/hephaestus/agent/sandbox/SandboxProperties.java

printf '\n== single-host compose guidance ==\n'
sed -n '60,105p' docker/self-host/compose.single-host.yaml

printf '\n== self-host env example ==\n'
sed -n '90,120p' docker/self-host/.env.example

printf '\n== install docs memory guidance ==\n'
sed -n '20,45p' docs/admin/install.mdx

printf '\n== search for maxConcurrentContainers ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' 'maxConcurrentContainers|concurrent sandbox|sandbox concurrency|4 GiB per sandbox|up to 4 GiB|one concurrent agent sandbox|five 4 GiB' server docs docker

Repository: ls1intum/Hephaestus

Length of output: 11915


The memory budget still misses worker sandboxes.

application-server + application-worker + webhook-server already reserve 11.5 GiB, and the worker still allows 5 concurrent sandboxes at 4 GiB each. That can push a shared host well past the “comfortably under RAM” guidance.

  • docker/compose.app.yaml#L96-L100 / #L299-L300: cap SANDBOX_MAX_CONCURRENT here or lower the JVM limits.
  • docker/compose.core.yaml#L11-L16: keep the webhook limit in the aggregate budget.
  • .changeset/jvm-memory-limits.md#L12-L16: add the sandbox ceiling and minimum host RAM to the operator guidance.
📍 Affects 3 files
  • docker/compose.app.yaml#L96-L100 (this comment)
  • docker/compose.app.yaml#L299-L300
  • docker/compose.core.yaml#L11-L16
  • .changeset/jvm-memory-limits.md#L12-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker/compose.app.yaml` around lines 96 - 100, The memory budget must
include worker sandbox concurrency. In docker/compose.app.yaml at lines 96-100
and 299-300, cap SANDBOX_MAX_CONCURRENT or lower the JVM memory limits so the
combined allocation remains within the host budget; retain the webhook memory
limit in docker/compose.core.yaml lines 11-16. Update
.changeset/jvm-memory-limits.md lines 12-16 with the sandbox ceiling and minimum
recommended host RAM.

@FelixTJDietrich
FelixTJDietrich force-pushed the perf/jvm-memory-limits branch from 81d3f36 to f66c063 Compare July 20, 2026 16:07
…iption

The application-server, application-worker and webhook-server containers had no
memory limit, so the Paketo memory calculator sized each JVM's heap against the
whole host. On a shared box the heaps summed past physical RAM (observed: an
8.4 GiB heap on webhook-server whose live set is ~150 MiB, plus 5.36 GiB on the
app-server, on an 11 GiB host), driving the machine into swap and leaving the
application server near-constantly in G1 concurrent GC and unresponsive.

Pin an explicit, per-service, env-overridable mem_limit so each JVM sizes its
heap for its own container. Defaults leave headroom for co-located services.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012stCYrymLyX9VZ8PrPUs88
@FelixTJDietrich
FelixTJDietrich force-pushed the perf/jvm-memory-limits branch from f66c063 to 7e218fe Compare July 21, 2026 08:09
@FelixTJDietrich
FelixTJDietrich merged commit 6faed3b into main Jul 21, 2026
22 of 23 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the perf/jvm-memory-limits branch July 21, 2026 08:11
@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation Preview

Preview has been removed (PR closed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci GitHub Actions, workflows, build pipeline changes infrastructure Docker, containers, and deployment infrastructure performance Speed, memory, or efficiency improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant