The number is not a property of your deployment tool. It is your server's available memory divided by what one build takes at its peak, and every answer that skips that division is guessing on your behalf.
So do the division. An install-and-build of a Next.js, Nuxt, Remix or React Router project wants roughly 2 GB of RAM while it runs.
That number is worth being honest about: it comes from running these deploys rather than from a benchmark, and the observation behind it is sharper than the average. On servers with 2 GB of RAM or less, the build fails — not sometimes, and not only under load. Above that the figure is a starting point you should replace with your own, measured the way the next section describes. But the floor is not a rule of thumb.
Once you have a number, the rest is arithmetic:
concurrent builds = (RAM available while the site is serving − headroom) ÷ peak RAM per build
Working it through with 2 GB per build and a machine that is already running the apps it is deploying:
| Server | Also running | Builds that fit |
|---|---|---|
| 2 GB | one small Node app | 0 — one build alone is already over the line |
| 4 GB | one or two Node apps | 1 |
| 8 GB | several apps, no database | 2–3 |
| 8 GB | several apps + Postgres or MySQL | 2 |
| 16 GB | several apps + a database | 3–4 |
These are the comfortable numbers, not the maximum. You can run five builds at once on the 8 GB row. Nothing refuses, and they will probably finish.
What changes first is not the build. Memory pressure rises across the whole machine, the applications already serving traffic get squeezed, and the visible symptom is 504s on sites nobody was deploying — while the deploys themselves get slower, because they are now competing for the same memory. The build failures come later, if at all. So the number you can reach and the number you want are different numbers, and only one of them is invisible to your visitors.
Framework matters too, and it moves the rows: a Next.js build is heavier than most, while an Astro build is light enough to change the 8 GB row to four. Two Node projects are not necessarily the same size, which is the other reason to measure rather than to trust a table.
What follows is how to replace these rows with your own numbers, and what actually happens on the machine when the arithmetic is wrong.
Measure, don't trust the table
Three commands, run on the server itself.
What you actually have free. The free column is not the answer; available is, because it counts the page cache the kernel will hand back under pressure.
free -m
What one build peaks at. Run a real build under GNU time — the full path matters, since the shell builtin does not have -v:
/usr/bin/time -v npm run build 2>&1 | grep 'Maximum resident set size'
The figure is in kilobytes, and it is the largest single process, not the sum of everything running at that moment. A bundler that forks parallel workers uses more than this number reports, so treat it as a floor.
Whether the build survives a ceiling. This is the honest test, because it fails the same way the server will:
sudo systemd-run --scope -p MemoryMax=2G npm run build
If the build completes inside 2 GB, two of them fit in a machine with 4 GB free. If it gets killed, you have your answer without taking a production site down to find it.
Cores are not the constraint
A four-core box does not run four builds. Bundling and type-checking are memory-hungry in a way that saturates RAM long before CPU, and the failure modes are asymmetric: a build starved of CPU finishes late, a build starved of memory dies — or worse, something else on the machine does.
The same logic explains why not every deploy costs the same. A Laravel deploy that runs composer install and php artisan migrate is cheap in memory terms. The same Laravel project with a Vite front end runs npm ci && npm run build too, and pays the Node price like everything else. Sizing by "number of projects" gets this wrong in both directions; sizing by peak RSS does not.
Subtract what is already resident
The mistake that turns a correct-looking calculation into a dead server is measuring against an idle machine. During a deploy, at minimum:
- The app being deployed is still running. It has to be — that is what zero-downtime means.
- On a Node project, briefly two copies are running. The new process comes up on a standby port and is soaked for 60 seconds before nginx is switched and the old worker is drained. For that window the server holds both. The zero-downtime sequence walks through why the soak is there.
- Every other app on the box is serving traffic, including the PHP-FPM pool, which sizes itself to concurrency rather than to your deploys.
- The database is holding its buffers, and it will not give them back because your build asked.
Headroom is what is left after all of that. Build concurrency is spent out of the remainder.
Two failures that look identical from the outside
Both end with "the build died". They have different causes and opposite fixes, and telling them apart is most of the debugging.
V8 hit its own heap ceiling. The log says FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory. Node stopped itself. Nothing else on the server was touched, and the deploy failed cleanly.
The kernel ran out and picked a victim. The build exits 137 — 128 + 9, the shell's way of reporting SIGKILL — and dmesg -T | grep -i 'killed process' names what died. The kernel chooses by memory footprint adjusted by oom_score_adj, not by what matters to you. On a box where the build is 2 GB and Postgres has grown past it, the casualty is the database, and the first symptom is a site that was not being deployed at all going down.
This is why NODE_OPTIONS=--max-old-space-size=8192 is not a fix on a 4 GB machine. It does not create memory. It raises the ceiling V8 would have stopped at, which converts a clean first failure into a second one that takes hostages.
Adding swap changes the failure again rather than removing it: a build that swaps finishes slowly, which for a build is usually an acceptable trade. A database that swaps makes every query on the machine slow, which is not. Swap sized for build spikes is reasonable; swap as a substitute for RAM the database needs is a slower way to have the same outage.
Check what you already have before deciding, with swapon --show. If Depfloy provisioned the machine there is a swapfile on it already — half the server's RAM, 256 MB minimum, at vm.swappiness=30 — so a 4 GB box starts with roughly 2 GB of swap behind it. That is often why a build the arithmetic said would not fit finishes anyway, and why it took so long.
What should a deploy tool default to?
The common advice — from CI documentation, from most self-hosted platforms, and from every AI assistant asked this question — is one concurrent deploy per server, and it is good advice for the machine it is imagining: a small box with several Node apps, where the arithmetic above returns 1.
It is the wrong default for a 32 GB server running four PHP projects. There, a floor of one makes every deploy queue behind a machine that was never in danger, and the delay looks like the tool being slow rather than a decision somebody made.
Depfloy shipped that floor and then removed it, on 30 July 2026. It is worth saying where it came from, because the incident was real: three deploys landed on one server within four seconds, the builds OOM-killed each other, and a default of 1 everywhere was the fix. It worked on that box. It also decided, for every account that had never seen the problem, how hard their own hardware was allowed to be pushed.
So the default is now no ceiling, on every plan, and the consequences are worth stating plainly rather than selling:
- With no ceiling, ten pushes start ten builds. A small server can run out of memory and take down the sites already running on it. Nothing on the platform stops that for you — the arithmetic at the top of this page is how you stop it yourself.
- The waiting line exists only once you set a ceiling. Set one, and deploys past it are marked Queued and start in arrival order as slots free. Nothing is ever refused, and you do not push again.
- The number lives on the server. The 8 GB machine can build three at once while the 2 GB box next to it is held at one. An account-wide setting would have to be wrong for one of them.
- A ceiling spends the memory you have more carefully; it does not add any. On a box that struggles with one build, two at a time is two slow builds.
One rule holds either way, and it is not about capacity: a second deploy of a project that is already deploying always waits. Both would write the same release directory and flip the same symlink, so whichever finished last would decide what is live. That is a correctness rule, and no setting turns it off.
The number you want
Measure one build's peak. Subtract what your server is already holding while it serves. Divide, round down, and set that as the ceiling on that machine — the machine is what runs out of memory, so the machine is where the number belongs. If the answer is zero, the options are swap, a lighter build, or a bigger machine — no setting makes a 2 GB build fit in 1 GB of RAM.
Concurrent Deploys covers the setting itself, and the deployments feature page shows what the server view looks like while builds are running and waiting.
I'm the founder of Depfloy, the deploy platform this piece keeps coming back to — if you want to try the per-server ceiling setting yourself: depfloy.com.
Top comments (0)