Build1 publisher3 min readPublished
npm ci OOM-killed a 1.6 GB production box while Cloudflare reported 521 and 522
Cloudflare's 521 and 522 look the same whether an origin is powered off, firewalled or thrashing, and it took a gap in syslog timestamps to pin a 10-hour outage on npm ci run against a 1612 MB VPS.
The Engineer · Build desk

What happened
- A small VPS serving a static sleep-cycle calculator stopped responding for 10 hours on September 18th, then went down again for 4 hours two days later.
- The operator traced both events to a single command run on the wrong machine: npm ci, executed against the production host.
- Cloudflare answered every request with 521 or 522, the codes for "web server is down" and "connection timed out".
- Syslog stopped taking writes in escalating blocks of 18, 23, 47, 50, 61 and finally 242 minutes, with the clock jumping forward each time logging resumed.
- The fix was a 4G swapfile, a sysctl drop-in setting vm.swappiness=10 and vm.vfs_cache_pressure=50, and earlyoom to force the kill decision sooner.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint An operator whose only outage signal is the CDN status code cannot narrow the fault at all, because the edge returns the same two numbers for a dead host, a firewalled one and one thrashing on reclaim.
- cost The misdiagnosis cost two days of attention spent on Cloudflare, DNS and nginx, all three of which turned out to be healthy while the box kept freezing.
- decision Anyone who copied swappiness=0 from database tuning guidance onto a small VPS has a config line to re-decide, because the setting does something different when there is no swap to suppress.
- exposure The other small sites sharing that 1612 MB host were unreachable for the full 14 hours of combined downtime, without anyone touching their code.
With swap at zero and vm.swappiness at zero, the kernel on that box had one reclaim option left: drop page cache [16]. It dropped, the pages were read back, it dropped them again. Under mounting pressure the machine ends up spending all of its time reclaiming and none of it running anything [16]. The box had nothing left to run [17]. The CPU idle figure stayed above 90 percent for the whole event [17].
Syslog is what dated it. /var/log/syslog takes writes from dozens of processes continuously, so a freeze stops the writes while the clock keeps going, and the next line lands with a visible jump in timestamps [8]. The author's point about that signal is narrow and correct: a gap has essentially one possible cause, which is nothing being able to write to disk [10]. The six gaps add up to 441 minutes, 7 hours 21 minutes of frozen time inside a 10-hour outage [22]. Each was longer than the one before, and the author takes that escalation as the mark of a machine running out of room, not a service that died [27].
kern.log named the process [11]. total-vm:12389432kB is about 11.8 GiB of address space, roughly 7.5 times the RAM in the machine [23]. anon-rss:605064kB is about 591 MiB, some five percent of that address space and a bit over a third of the box's memory [24]. total-vm is virtual address space, not resident memory, the author notes, and npm ci was holding roughly 600 MB of real pages when the kernel killed it [13]. Provision off total-vm and you buy about 20 times the memory the process actually touched [28]. The same oom-kill line, with a different PID, was already in the log three days earlier [12].
Cloudflare's 521 and 522 say only that the edge could not reach the origin [7]. An origin that is powered off, one that is firewalled and one that is alive but unable to answer all produce the same pair [7]. The post-mortem puts a price on that ambiguity: two days of wrong assumptions, and a second outage the author says ran four times longer than it needed to [6]. In wall clock the second outage was the shorter of the two, 4 hours against 10 [25]. The post does not publish a timeline for it, so the multiplier is the author's estimate of avoidable downtime.
All three mitigations still leave npm ci free to run on a production host, and the author names the command run on the wrong machine as the cause [3]. "The core problem was never that npm ci used a lot of memory. It's that the machine spent hours failing to cope before the OOM killer finally acted," the author wrote [20].
For the numbers to transfer you need the shape of this box: swap at zero, and a working set that plus roughly 600 MB of package manager puts the machine past 1612 MB [14][24]. The swappiness=0 setting came out of database tuning guidance, written for a box with plenty of RAM where dropping cache beats paging out a working set [15].
What to watch
- Whether the same box survives the next npm ci now that 4 GB of swap and earlyoom are in place; the post-mortem stops at the fix.