BuildNot yet confirmed elsewhere1 publisher3 min readPublished
Build concurrency on one VPS is a division problem, and the app you serve pays the remainder
Roughly 2 GB per JavaScript build, a hard failure floor at 2 GB of RAM, and a formula that subtracts headroom first. Exceed it and the 504s land on sites nobody was deploying.
The Engineer · Build desk
What happened
- An install-and-build of a Next.js, Nuxt, Remix or React Router project wants roughly 2 GB of RAM at peak.
- Below 2 GB of RAM the build fails outright, not intermittently and not only under load.
- The sizing rule is available memory while the site is serving, minus headroom, divided by measured peak per build.
- The first thing to break is other traffic, showing up as 504s on sites that were not being deployed.
Why it matters
- exposure Your instrumentation points at the wrong subsystem: the deploy pipeline reports success while the capacity error surfaces as unrelated app latency, so the incident gets triaged as an app problem.
- constraint Core count stops being a sizing input, which means paying for more vCPU buys no additional concurrency on a memory-bound box.
- decision The choice is whether to establish the ceiling in a scoped test run or to discover it during a live deploy with customers attached.
- cost The Node bill is charged per front end rather than per project, so adding a bundler to a cheap PHP deploy quietly reprices the whole server.
The numerator is where this usually goes wrong, because it gets read off an idle box. `free -m` prints two columns and only one of them is the answer: `available`, because it counts the page cache the kernel will hand back under pressure [5]. Then subtract everything that is only true mid-deploy. The app you are replacing is still running, which is what zero-downtime means [9]. On a Node project the new process comes up on a standby port and is soaked for 60 seconds before nginx switches and the old worker drains, so for that window the machine holds both copies [10]. The PHP-FPM pool sizes itself to traffic rather than to your deploy schedule [11], and the database will not give its buffers back because a bundler asked [12]. Build concurrency is spent out of whatever survives that subtraction [13], which means the real peak during a soak is the build plus two resident copies of the app, not the build figure alone [18].
The denominator needs the same suspicion. `/usr/bin/time -v` reports the maximum resident set size of the largest single process, not the sum of what was running, so a bundler forking parallel workers goes past it and the number is a floor [7]. The full path matters, since the shell builtin has no `-v` [6]. What settles it is a ceiling: `systemd-run --scope -p MemoryMax=2G npm run build` fails the way the server will fail [14], and a build that completes inside 2 GB means two of them fit in 4 GB free [15].
The comfortable rows are not the maximum, and the gap between them is where the damage lives. Five builds start on the 8 GB row and will probably finish [21]. At roughly 2 GB each that is about 10 GB of demand on a box that holds 8, before the served apps and the database are counted [22]. Nothing refuses. Memory pressure rises across the machine, the apps already serving traffic get squeezed, the deploys themselves slow down competing for the same memory, and the symptom you see is 504s on sites nobody was deploying [3]. Build failures come later, if they come [4].
This is also why core count is a bad input. Bundling and type-checking saturate RAM long before CPU [16], and the two starvation modes are not symmetric: a build short of CPU finishes late, a build short of memory dies, or kills something else on the box [17]. Counting projects gets sizing wrong in both directions; counting peak RSS does not [8]. A Laravel deploy that is `composer install` and a migration is cheap in memory terms, but add a Vite front end and it runs `npm ci && npm run build` and pays the Node price like anything else [20]. Framework choice moves the rows too: Astro is light enough to take the 8 GB row to four, while Next.js is heavier than most [19]. And the 2 GB floor is not a rule of thumb [25]. At that tier one build wants the entire machine, leaving nothing for the kernel, the app still serving, or the database [26].
What to watch
- Whether the roughly 2 GB per-build figure survives the next framework majors, since a bundler that adds worker parallelism moves the divisor with no announcement.
- Per-framework peak RSS numbers measured under a MemoryMax ceiling rather than on idle hardware, which would replace the published rows with something defensible.
- Whether hosts and deploy tools start enforcing per-deploy memory ceilings, so a runaway build is killed instead of the neighbouring app.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence38
- Adoption
- Insufficient
- Hype gap+28
- Incentives62
- Confidence45
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Concurrent build capacity is not a property of the deployment tool; it is the server's available memory divided by what one build takes at its peak.
- [2]
The stated formula is: concurrent builds = (RAM available while the site is serving - headroom) / peak RAM per build.
- [3]
What changes first is not the build: memory pressure rises across the whole machine, applications already serving traffic get squeezed, deploys get slower as they compete for the same memory, and the visible symptom is 504s on sites nobody was deploying.
- [4]
The build failures come later, if at all.
- [5]
In free -m the 'free' column is not the answer; 'available' is, because it counts the page cache the kernel will hand back under pressure.
- [6]
Peak usage is measured with /usr/bin/time -v npm run build piped to grep for 'Maximum resident set size'; the full path matters because the shell builtin does not have -v.
- [7]
The reported figure is in kilobytes and covers the largest single process, not the sum of everything running, so a bundler that forks parallel workers uses more than it reports and the number should be treated as a floor.
- [8]
Sizing by number of projects gets this wrong in both directions; sizing by peak RSS does not.
- [9]
Measuring against an idle machine turns a correct-looking calculation into a dead server, because during a deploy the app being deployed is still running, which is what zero-downtime means.
- [10]
On a Node project two copies run briefly: 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, and for that window the server holds both.
- [11]
Every other app on the box is serving traffic during a deploy, including the PHP-FPM pool, which sizes itself to concurrency rather than to your deploys.
- [12]
The database is holding its buffers during a deploy and will not give them back because your build asked.
- [13]
Headroom is what is left after the running app, the duplicate process, the other apps and the database, and build concurrency is spent out of the remainder.
- [14]
The honest test is sudo systemd-run --scope -p MemoryMax=2G npm run build, because it fails the same way the server will, without taking a production site down to find out.
- [15]
If the build completes inside 2 GB, two of them fit in a machine with 4 GB free.
- [16]
A four-core box does not run four builds: bundling and type-checking saturate RAM long before CPU.
- [17]
The failure modes are asymmetric: a build starved of CPU finishes late, while a build starved of memory dies, or something else on the machine does.
- [18]
During the 60 second soak window the machine's demand is the build peak plus two resident copies of the application, so the deploy's true peak is higher than the build figure alone.
- [19]
Framework choice 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.
- [20]
A Laravel deploy running composer install and php artisan migrate is cheap in memory terms, but the same project with a Vite front end also runs npm ci && npm run build and pays the Node price.
- [21]
The published rows are comfortable numbers rather than maximums: five builds can run at once on the 8 GB row, nothing refuses, and they will probably finish.
- [22]
Five concurrent builds at roughly 2 GB peak each is about 10 GB of demand on an 8 GB machine, more than the box holds before the served apps, the PHP-FPM pool and the database buffers are counted.
- [23]
An install-and-build of a Next.js, Nuxt, Remix or React Router project wants roughly 2 GB of RAM while it runs.
- [24]
The roughly 2 GB figure comes from running these deploys rather than from a benchmark, and the observation behind it is sharper than the average.
- [25]
On servers with 2 GB of RAM or less the build fails, not sometimes and not only under load; the floor is not a rule of thumb.
- [26]
On a 2 GB server a single build at 2 GB peak leaves nothing for the kernel, the app still serving or the database, which is why failure at that tier is categorical rather than occasional.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toHow Many Builds Can Run at Once on One VPS
1 article · August 26, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.