On this page
How Much RAM Does a Web App Actually Need?
A practical way to size memory from the runtime, the workload, and the concurrency — instead of guessing and watching the process get killed.
Quick answer
- Size from three numbers: the runtime’s own overhead, memory per request or connection, and peak concurrency. Multiply, then add headroom.
- Interpreted runtimes are heavy per instance: a Node.js process starts around tens of megabytes, the JVM and similar are larger, and each worker you run multiplies the baseline.
- Memory per request is dominated by buffered payloads, in-memory caches, and session state — all things AI-generated code tends to hold in process.
- Leave headroom for the operating system, the database, and the cache if they share the machine. Running at ninety per cent memory is how you get out-of-memory kills under a traffic spike.
- Measure with the real workload rather than the development one; memory behaviour under load is not visible in a local test.
How much RAM does a web app need?
Work it out from three components rather than from a rule of thumb. First, the fixed footprint: the runtime, the framework, and the application code, which for an interpreted runtime is the process before it serves anything. Second, the variable footprint: whatever is retained per concurrent request or connection, including in-memory caches, session state, and any buffered request or response bodies. Third, the operating system, the reverse proxy, the database, and any background workers sharing the machine.
Multiply the second component by peak concurrency, add the rest, and then leave a margin. A service holding half a megabyte of state per connection and serving two hundred concurrent connections is using a hundred megabytes before the runtime is counted. That is the arithmetic that determines sizing, and it is the arithmetic that a generic ‘start with one gigabyte’ recommendation skips.
Where the memory actually goes
Four places dominate in practice. The runtime baseline: a Node.js process is relatively light, the JVM is heavy and its heap needs explicit bounds, Python’s interpreter and framework are in between, and each worker process you add multiplies all of it. Buffers: reading a whole file or response into memory instead of streaming it turns a small upload limit into a large memory requirement. Caches: an in-process cache with no size limit grows until it is the largest thing on the machine. And sessions: storing session data in process memory means every user’s state lives on one server and is lost when it restarts.
The last two are the usual culprits in generated code, because they are the simplest correct-looking implementation. An unbounded dictionary as a cache and sessions in process memory both work perfectly in development and both scale badly, and neither failure appears until concurrency rises.
A worked estimate
Take a small API on a managed platform, sharing its server with a database. The runtime and framework might occupy around a hundred megabytes; a reverse proxy and the operating system another hundred; the database a few hundred once it has buffers and a working set. Then the application holds perhaps half a megabyte per active request, mostly payloads and per-request state, so two hundred concurrent requests add roughly a hundred megabytes. That lands comfortably under a gigabyte for the application, with the database being the larger consumer.
Now change one assumption and the answer changes sharply. If the app caches a thousand objects of ten kilobytes each in process, that is ten megabytes, fine; if the cache is unbounded and traffic doubles, it is not fine. The estimate is not a formula to trust but a way to find the line item that dominates, and in most applications that line item is a cache or a buffer nobody measured.
# See what a process actually uses right now (resident set size, in KB).
ps -o pid,rss,comm -C node,python,gunicorn,postgres
# Container limit and current usage, if you are containerised.
cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory.current 2>/dev/null || cat /sys/fs/cgroup/memory/memory.usage_in_bytes
# Watch memory move while a load test runs, not while the app is idle.
watch -n 2 'ps -o rss,comm -C node | tail -n +2 | sort -rn | head'Why running close to the limit is expensive
Memory is the resource that produces a hard failure rather than a slow one. When the kernel or the container runtime cannot allocate, the process is killed — often the one with the largest resident set, which is usually your application. The symptom is a restart under traffic, which looks like a crash loop rather than a capacity problem, and the log line that matters is usually from the kernel rather than the application.
So keep a margin. Twenty to thirty per cent headroom absorbs spikes and memory fragmentation, and it is cheap compared with the debugging time an out-of-memory kill costs. The corollary is that if you are consistently above seventy or eighty per cent, that is not a stable configuration even if it has survived so far; it is a spike away from an outage.
Where this bites vibecoders
AI-generated code is prone to two memory shapes: an in-process cache that grows without bound, and session state held in the application rather than in a shared store. Both are the shortest path to working code and both make memory consumption a function of traffic. A review pass that asks ‘what grows with users?’ and ‘what happens if this cache never expires?’ catches both, and neither requires a profiler.
Where AI coding assistants get this wrong
- Using an unbounded in-process cache and never evicting entries.
- Storing session state in application memory, tying users to one server and losing state on restart.
- Reading whole files or responses into memory instead of streaming them.
- Sizing the server from development usage, where concurrency is one.
- Setting a runtime heap limit above the container’s memory limit, inviting an external kill.
- Ignoring the database and proxy when the same machine hosts everything.
Checklist
- Measure the process’s resident memory with no traffic.
- Find everything retained per request or connection.
- Cap every in-process cache and check the eviction policy.
- Move session state to a shared store if more than one server runs.
- Estimate peak concurrency and multiply the per-request figure.
- Keep twenty to thirty per cent headroom and alert before you reach it.
FAQ
How much RAM does a Node.js server need?
The runtime and a small application typically start in the low hundreds of megabytes, and the requirement is then driven by concurrency and by whatever is cached in process. Binding a heap limit and watching resident memory under load tells you the real answer in minutes; the starting number tells you very little.
Why does my process get killed under load?
Almost always because memory grew past a hard limit — a container limit, or the host’s available memory — and the kernel’s out-of-memory killer chose the largest process, which is usually the application. The fix is to find what grows with traffic: an unbounded cache, buffered payloads, or per-connection state.
Is it cheaper to add RAM or to move the database off the server?
Usually moving the database or the cache to a managed service, once the application is modest, because it removes a large and unpredictable consumer from the same memory budget and makes both easier to size. Adding RAM to a shared box postpones the question without changing the coupling.
How much headroom should I leave?
Around twenty to thirty per cent of the limit, and more on a shared machine. Memory does not degrade gracefully: past the limit, the process is killed rather than slowed. Sustained usage above roughly eighty per cent is a configuration that has not failed yet, not one that is safe.
Related topics
- What Is RAM (and Why Does Your App Run Out of Memory)?
- Why Does My App Work Locally but Not in Production?
- How to Choose a VPS Size for an AI-Built App
- Why Does My Container Keep Restarting Every 30 Seconds?
- What Is a Memory Leak (and Why Does Your App Get Slower the Longer It Runs)?
- Latency Numbers Every Developer Should Know