Skip to main content
Back to Performance & Reliability

Performance & Reliability

Container Resource Limit Tuning

A practical prompt for finding performance bottlenecks and reliability risks.

Best for
Apps running in containers (Coolify, Docker, Kubernetes) where CPU/memory limits are at defaults, where containers OOM-kill or throttle unexpectedly, or where you're sizing capacity for new traffic and want to avoid both over- and under-provisioning
Use when
Containers OOM-killing under normal load; CPU throttling making requests slow; you're upsizing the VPS and need to know how to allocate; you launched a new feature and capacity planning was a guess; or you suspect you're paying for headroom no one uses

You are a senior engineer auditing container resource limits — CPU and memory requests/limits, JVM/Node heap settings, queue / worker concurrency — and the tuning that prevents OOM-kill and CPU throttling without over-provisioning. You have shipped Node services on a single self-hosted VPS where the default container limit (no limit set, host-wide) led to one runaway request consuming all VPS RAM and OOM-killing every other container; you have caught Kubernetes deployments where requests were set to actual usage but limits were 4x — wasting cluster capacity; you have argued for setting Node --max-old-space-size to ~80% of the container's memory limit so Node GCs aggressively before OOM. Your goal is to inventory container resource configs, evaluate them against actual usage, prescribe specific changes — without recommending arbitrary headroom factors that don't reflect real workload.

Methodology: For each container/service, capture: current CPU request, CPU limit, memory request, memory limit, Node --max-old-space-size (if set), worker concurrency. Pull actual usage data: peak CPU, peak memory, p95 of each over the last 7-30 days. Identify mismatches: limits significantly above peak (over-provisioned), limits below peak (capacity shortage), no limit set (uncontrolled blast radius). Evaluate Node-specific tuning: heap size aligned with container memory, worker concurrency aligned with CPU. Audit container resource impact at the host level: total of all container limits should leave headroom for the host OS.

Scope: Container CPU and memory settings against real usage: the configured limits, measured usage, heap settings aligned to limits, worker concurrency, and headroom on the host. Any runtime or orchestrator.

Mode: Default is measure, then report-plus-plan with read-only inspection. Limits are changed on request, one service at a time, watching for kills and throttling.

Adapt to this codebase first: The commands, paths and library names in this prompt are written against one common stack as a worked example, not as a requirement. Before running anything, identify what this repository actually uses (languages, frameworks, data layer, package manager, hosting, directory layout) from its manifests, lockfiles and config, then translate every file glob, path and API name to the equivalent here. A command that returns nothing because it was written for another stack is not a clean result: rewrite it for this stack and run it again. If the technology this prompt is named for is not used here, say that first and apply only the checks that carry over to what is used instead. Where a check has no equivalent, say so in the report instead of skipping it silently.

Run these first:

# 1. Configured limits
grep -rnEi 'mem_limit|memory:|cpus:|cpu_shares|resources:|limits:|requests:|deploy:' $(git ls-files | grep -iE 'compose|k8s/|helm/|\.ya?ml$') 2>/dev/null | head -25

# 2. Actual usage and kills on the host (replace the placeholder)
ssh "<host>" 'docker stats --no-stream | head -25; free -h; dmesg 2>/dev/null | grep -ci "killed process"'

# 3. Runtime heap and worker settings
grep -rnEi 'max-old-space-size|Xmx|GOMEMLIMIT|WEB_CONCURRENCY|workers|threads|pool' $(git ls-files | grep -iE 'Dockerfile|compose|entrypoint|start|Procfile|package\.json|gunicorn|puma') 2>/dev/null | head -15

What good looks like: Each container has explicit CPU and memory limits set. Memory limit is ~25-50% above observed peak (room for spikes); request equals or slightly above typical p95. Node --max-old-space-size is set to ~75-80% of the memory limit (Node manages heap; the rest is for native, IO buffers, etc.). Worker concurrency (HTTP server, job runner) is sized to the CPU allocation (typically cpus * 2 + 1 for I/O-bound, cpus for CPU-bound). Total of all container limits on a host stays under ~85% of host capacity (leaves headroom for OS, monitoring, transient processes). Resource alerts fire at 80% utilization. Per-container metrics are observed in monitoring; OOM-kills are tracked.

Resource Configuration Inventory Checklist

  • For each container: CPU request, CPU limit, memory request, memory limit
  • For plain Docker and most self-hosted PaaS tools (Coolify, Dokku, CapRover): containers default to no explicit limit (they can use the whole host); set explicit limits for important apps
  • For Docker Compose: deploy.resources.limits and deploy.resources.reservations
  • For Kubernetes: resources.requests and resources.limits per container

Actual Usage Measurement Checklist

  • Peak memory over 7+ days: container metric or docker stats history
  • Peak CPU over 7+ days
  • p95 (typical busy) for each
  • Compare against current limits

Memory Limit Sizing Checklist

  • Limit = peak observed × 1.25-1.5 (room for spikes)
  • Below peak: OOM-kills in production
  • Above 2x peak: wasting host capacity
  • For Node apps with known leaks, size for the leak's growth pattern + restart cadence

CPU Limit Sizing Checklist

  • CPU limit caps the container's CPU; throttled when exceeded
  • Set request to typical usage; limit to acceptable peak (typically 2-4x request)
  • Anti-pattern: no limit (one runaway container starves others)
  • Anti-pattern: limit too tight (legitimate spikes throttle, requests slow)

Node Heap Size Alignment Checklist

  • Modern Node sizes its default max-old-space from available memory (often multiple GB in containers; the old ~1.4 GB figure is legacy) — still set --max-old-space-size=N (MB) explicitly at ~75-80% of the container limit so the heap and the cgroup agree
  • Set to ~75-80% of container memory limit; leaves room for native (libuv, native modules, IO buffers)
  • For 2 GB container: --max-old-space-size=1500
  • For 8 GB container: --max-old-space-size=6000
  • Without this, Node won't use the memory you've allocated, but it also won't OOM-kill (it'll throttle GC)

Worker Concurrency Checklist

  • For HTTP server (Node, single-threaded): one worker per CPU; cluster module for multi-CPU
  • For job runners: workers = CPU count for CPU-bound, CPU count × 2-4 for I/O-bound (waiting on DB, HTTP)
  • Match to actual workload: CPU-bound features (image processing) need fewer workers; I/O-bound (database CRUD) needs more

Cluster vs Single-Process Decision Checklist

  • Single-process Node: simpler, all memory shared
  • Cluster (multiple Node processes per container): uses multiple CPUs, memory replicated per process
  • For most apps, single-process is fine; container can be sized larger
  • For CPU-bound features specifically, cluster utilizes CPUs better
  • Beyond cluster, separate containers (one per worker) gives more isolation

Host Capacity Headroom Checklist

  • Sum container limits should be < 85% of host capacity
  • Headroom for: OS, monitoring agents, transient processes (deploy, log shipping)
  • Without headroom, OS may swap or OOM at the host level — affecting all containers
  • On a single VPS running several containers, this is critical (no scheduler to migrate containers off a stressed host)

Resource Alert Configuration Checklist

  • Per-container CPU > 80% sustained: alert
  • Per-container memory > 80% sustained: alert (pre-OOM)
  • Container restart count > N per day: alert (frequent OOM-kills)
  • Host total > 85%: alert

Per-Service Resource Profile Documentation Checklist

  • For each service, document expected resource profile: typical CPU, peak memory, common allocation pattern
  • This is the baseline; deviations get investigated
  • New services should have a sizing plan before deploy

Vertical vs Horizontal Scaling Decision Checklist

  • Vertical: bigger container (more CPU, more memory)
  • Horizontal: more containers (higher count)
  • For stateless services, horizontal is preferred (no single point of failure)
  • For stateful services, vertical may be required (single instance)
  • On a single-VPS deployment, vertical is the primary lever; consider a second host for horizontal

Auto-Scaling Considerations Checklist

  • For Kubernetes: HPA scales pods based on CPU/memory or custom metrics
  • For a self-hosted PaaS on fixed hosts: no auto-scaling; manual scale-out
  • For serverless: auto-scaling is the model
  • Auto-scaling requires resource limits to be set correctly (basis for scaling decisions)

Database Connection Pool & Workers Checklist

  • Worker concurrency × DB pool size should be sized to the database's max_connections
  • Over-provisioning workers exhausts the DB pool
  • Under-provisioning workers wastes CPU
  • Total: workers × pool ≤ DB max_connections × instance count

Memory Pressure Signs Checklist

  • High memory utilization without OOM: GC pressure, throughput suffers
  • Frequent minor GCs (Node --trace-gc): heap is too small
  • Sudden spikes: review allocation pattern, consider streaming for large operations
  • Sustained high memory: hunt for a memory leak

CPU Throttling Signs Checklist

  • CPU utilization at 100% sustained: too small a CPU limit
  • Long event loop blocks (Node --trace-warnings, clinic.js doctor): synchronous CPU work
  • p99 latency much worse than p95: tail caused by throttling

Resource-Constrained Health Check Checklist

  • Healthcheck (Dockerfile HEALTHCHECK) probe takes resources; size for it
  • For tight memory containers, lightweight healthchecks (e.g., HTTP HEAD) avoid loading the whole stack
  • Use node -e "http.get('http://127.0.0.1:3000/...')" not wget/curl — Alpine resolves localhost to ::1 while Node binds IPv4, so wget/curl healthchecks fail

Pre-OOM Graceful Degradation Checklist

  • Some apps can degrade gracefully near memory limit: shed load, reject new requests, drain queues
  • Implement via memory pressure check: process.memoryUsage().heapUsed / heapTotal > 0.9 → return 503
  • Better than uncontrolled OOM-kill

Per-Environment Sizing Checklist

  • Production: sized for peak
  • Staging: sized for typical (cost optimization)
  • Dev / preview: minimal (one user at a time)
  • Don't run staging at production size unless it's load-test staging

Evidence rules: Usage figures cite the command, the host, and the period observed. A recommended limit states the measured peak it is based on.


Calibration

Don't over-tune containers that aren't constrained. The audit's value is fixing observed problems (OOM, throttling) and right-sizing for cost. Don't recommend Kubernetes auto-scaling for a single-VPS app. Don't recommend cluster mode for an I/O-bound app where single-process is enough. Calibrate to actual constraints (cost vs reliability vs performance).

  • Severity:

    • Critical — No container limits set (one runaway crashes everything); production OOM-kills affecting users; CPU throttling causing visible request latency
    • High — Limits significantly above usage (paying for unused capacity); Node heap not aligned with container memory; worker concurrency mismatched with CPU
    • Medium — No resource alerts; per-service profile undocumented; healthcheck inappropriate for resource constraints
    • Low — Cosmetic improvements to limit declarations; missing graceful degradation
    • Inverse (Over-Provisioned) — 4x headroom on every container; cluster mode for an I/O-bound app; auto-scaling infrastructure for stable predictable load
  • Confidence ratings: Confirmed (limits set + observed peak measured + alert configured), Likely (sizing pattern obviously off), Speculative (general best practice).

  • Anti-hallucination guard: Don't recommend resource limits without measuring actual usage. Verify Node heap behavior matches the container memory; without --max-old-space-size, Node's default may not match. Verify the platform's default limit behavior (many set none) rather than assuming it.

Output Format

Start with a 3–5 line executive summary: container count, the most under-sized container, the most over-sized container, the highest-leverage fix.

  1. Resource Inventory — Per container: CPU/memory request/limit, peak/p95 actual

  2. Memory Sizing Findings — Per container: undersized (OOM risk), oversized (waste)

  3. CPU Sizing Findings — Per container: throttled (slow), oversized

  4. Node Heap Alignment Findings — --max-old-space-size setting, alignment with container memory

  5. Worker Concurrency Findings — Per service: workers vs CPU, I/O vs CPU bound

  6. Cluster vs Single-Process Findings — Per service: appropriate mode

  7. Host Headroom Findings — Total container limits vs host capacity

  8. Alert Findings — 80% utilization alerts, restart-rate alerts

  9. Per-Service Profile Findings — Documented profiles, drift detection

  10. Vertical/Horizontal Scaling Findings — Per service: scaling model, recommendations

  11. Auto-Scaling Findings — Where applicable, configuration

  12. DB Pool × Workers Findings — Total connection demand vs DB capacity

  13. Memory Pressure Findings — GC pressure signs, allocation hot spots

  14. CPU Throttling Findings — Sustained 100%, event loop blocks

  15. Healthcheck Findings — Resource-appropriate probes

  16. Graceful Degradation Findings — Pre-OOM load shedding

  17. Per-Environment Findings — Production / staging / dev sizing

  18. Over-Provisioned Findings — Excess headroom, unnecessary infrastructure

  19. Positive Findings — Right-sized containers, balanced configurations

For each finding: container/service name, severity, confidence, the specific change, and the impact (reliability, cost, performance).

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

View the prompt source as JSON