Build1 distinct publisher3 min readUpdated
A developer watched fpm-status in real time and reduced pm = dynamic to a single rule. Read it that way and an oversized max_children stops looking like insurance.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A developer writing on dev.to sat down with a live PHP-FPM pool and reduced `pm = dynamic` to one rule: roughly every second the master counts idle workers, forks one if there are fewer than `min_spare_servers`, kills one if there are more than `max_spare_servers`, and otherwise does nothing [2]. That matters because the loop never consults `max_children` except as a ceiling, which means the large ceiling most teams set defensively is doing far less work than they think.
The asymmetry is where the behaviour lives. Booting with `start_servers = 5` produced six workers almost immediately [3]: one request made four workers idle, which is below the minimum of five, so the master forked; when the request finished, six idle workers were still under the maximum of fifty, so nothing was killed [4]. FPM never returns to `start_servers`, because the kill rule only fires above `max_spare_servers`, and between the two bounds the pool simply stays wherever demand last pushed it [5]. Two seconds after a restart the author's status output showed 5 total, 4 idle, 1 active [6]; thirty-two seconds later it showed 6 total and 99 accepted connections [7], about 2.9 per second [23], all of it a metrics exporter polling `/fpm-status` and keeping one worker permanently busy [9]. The status request is itself served by a worker [8].
Under a k6 run with 100 virtual users, the pool sat at 50 total when quiet, which is exactly `max_spare_servers`, grew to 72 during the burst, and glided back down to 50 over roughly a minute [10]. Shrinking is deliberately slow, roughly one worker per second, so a dip followed by another spike does not trigger a fork storm [11]. Note what the ceiling did during all of this: nothing. The configured ceiling of 300 [1] is about 4.2 times the observed peak of 72 [21].
The counters say the same thing more bluntly. `max active processes` is a high-water mark of workers simultaneously busy, and comparing it to `max_children` shows how close the pool has come to the wall [17]. `max children reached` counts the occasions when every worker was busy and a new request had nowhere to go; anything above zero means users waited [18]. `listen queue` shows requests waiting for a worker right now [19]. All of these reset on restart or reload [20], which is the usual reason a team believes it has never hit the ceiling.
Then the arithmetic. Measured warm and under load, the author's Laravel app ran 51 processes at 1866 MB RSS, about 36.6 MB each [12]; fresh workers start near 14 MB and grow to roughly 37 MB, and a heavy CMS can reach 150-250+ MB per worker [13]. Applying `(RAM for PHP x 0.9) / avg process size` [14] to 7.7 GB gives about 187 children, against a configured 300 that would need about 11 GB, more than the box has [15]. The configured ceiling exceeds the RAM-derived one by about 60 percent [22]. The author is candid that his own config flunks his own math, and that he got away with it only because real peak was 72 workers [16] and therefore about 2.7 GB [24].
Worth watching in your own pools: whether `max children reached` is non-zero since the last reload, and whether the RSS you sized from was measured warm rather than at fork time [13][20].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The example pool config is pm = dynamic with pm.max_children = 300, pm.start_servers = 5, pm.min_spare_servers = 5, pm.max_spare_servers = 50, pm.max_requests = 1000.
FPM's master process runs one check roughly every second: if idle workers are fewer than min_spare_servers, fork one; if more than max_spare_servers, kill one; otherwise do nothing.
The author booted FPM with start_servers = 5 and immediately saw 6 workers.
Timeline: at t=0, 5 idle workers and no action; at t=1 one request leaves 4 idle, which is below the minimum of 5, so the master forks; at t=2 there are 1 busy plus 5 idle; at t=3 the request finishes leaving 6 idle, which is not above the maximum of 50, so nothing is killed and the pool stays at 6.
FPM never shrinks back to start_servers; the kill rule only fires above max_spare_servers, so between the two bounds the pool stays wherever demand pushed it, ratcheting up easily and trimming lazily.
Status output two seconds after a restart showed start since: 2, idle processes: 4, active processes: 1, total processes: 5.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Concrete first-hand measurements, single unreplicated setup
Nearly every claim is backed by pasted fpm-status output, a reconstructed per-second timeline, a k6 run and RSS figures, which is unusually specific for a blog post and mostly reproducible by any reader with the status endpoint enabled. It is nonetheless one author, one box, one framework, with no PHP-FPM version, kernel or hardware disclosure and no independent corroboration in the cluster, and the memory budgeting leans on summed RSS without addressing shared pages.
One operator's own pool, no third-party uptake
The only usage signals are the author's own environment: a Laravel pool of ~51 workers on a 7.7 GB memory envelope and a k6 benchmark he ran himself. Nothing in the sources documents other deployments, fleet-wide practice, tooling adoption or how common the oversized-ceiling pattern is, so adoption is real but confined to a single self-reported installation.
Slightly understated relative to its own evidence
The framing is mechanism-first and self-incriminating: the author concludes his own max_children of 300 flunks his own formula and that he escaped OOM only because peak demand was 72 workers. There is no product, benchmark-winning claim or vendor comparison to inflate. Mild overreach exists in generalizing single-box numbers into rules of thumb (the 0.9 factor, 'anything above 0 means real users waited'), which is why the gap is only slightly negative rather than strongly so.
Practitioner post with no commercial stake shown
The sole source is an individual developer write-up on a community publishing platform; no vendor, sponsor, product, pricing or hiring pitch appears anywhere in the material, and the piece criticizes the author's own configuration. Residual incentive is reputational and engagement-driven, which can favour clean single-rule framings and quotable absolutes over caveats.
Confident on mechanism, weak on generality
The behavioural claims - the one-second idle check, the 5-to-6 ratchet, parking at max_spare_servers, the roughly one-worker-per-second trim - are internally consistent, directly observed and easy for a reader to verify, so confidence in them is high. Confidence in the numeric guidance and in how widely the oversized-ceiling pattern applies is much lower: one publisher, one app, one benchmark, no version or platform detail, and no cross-source check available in this cluster.
build
The guard that worked in tests and still wrote 2,684 live records1 distinct publisher
build
Every viewer hits your HLS key endpoint in the same second, and almost nobody tests it1 distinct publisher
build
Once the question needs a cube, you own the parser1 distinct publisher
build
Thirty lines of Doctrine filter, and the query paths where it is simply not there1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 20, 2026