Build1 distinct publisher3 min readPublished
Worker occupancy is arrival rate times service time, which is why an in-controller Redis hit only moves the ceiling and an edge proxy is the only layer that stops a request before a worker is allocated.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Occupancy is arrival rate times service time. The throughput formula in the dev.to post is a rearrangement of that, and the pool size on the top of the fraction is itself a memory budget [5]. Check the stated range: 60 workers at 70MB is 4.2GB, 100 at 40MB is 4.0GB [16]. Both ends reserve about half of an 8GB box for PHP and leave the rest to Nginx, the database and the kernel [4]. The directive is where you type the number. The RAM is where it comes from.
Now run the mixed case the post skips. Suppose the 1,500 req/sec spike [6] is 90% cacheable reads answered in 20ms [8] and 10% writes and uncacheable endpoints at 200ms [5]. Concurrency is 1,350 x 0.02 = 27 workers for the reads and 150 x 0.2 = 30 workers for the rest, so 57 of 100 are busy [15]. By the post's own arithmetic the in-controller Redis path has 3.3x headroom over the spike it is presented as failing to survive [17]. The narrower claim is the true one: the uncacheable tenth costs more worker-time than the cached ninety percent, and a mean occupancy of 57 is not a safe operating point when service times vary, because queueing starts long before the last worker is taken.
Read the controller code and a second problem shows up. It does a Redis get, and on a miss runs an eager-loaded findOrFail and a setex with a one-hour TTL [9]. There is no lock. Every concurrent request that arrives after that key expires misses, and every one of them runs the query. Request coalescing at the edge is the genuinely good part of the design [11], and it is the same primitive you would otherwise build in PHP with a mutex, which is a duller ticket than a proxy deployment.
The two marquee numbers are claims about someone else's traffic. The 12ms figure [1] holds for a cache hit served from a nearby POP [14]; the bypass path for writes still allocates a worker at full service time [10]. The 95% offload figure [1] requires 95% of requests to be GETs whose responses repeat inside the TTL. For a product catalog that is credible. For an authenticated dashboard, per-user responses fragment the key space and your offload is whatever your cardinality allows. The source measures neither.
Then the adoption cost. The recommended headers are max-age=86400 with stale-while-revalidate=300, plus a vendor-specific X-ApexCache-Tags list emitted by the controller [12]. That moves freshness out of the worker and into your purge path: the tag write on admin update [13] becomes a correctness dependency, and if it fails, a wrong price stays live at the edge for up to a day while origin logs show nothing at all. You are trading a cache lookup you can read in a stack trace for an invalidation contract you have to monitor separately. In a read-heavy catalog with a reliable purge hook, that is the right trade. In an app where most responses are per-user, you are paying the coupling and getting back the fraction of traffic that is actually shared.
Ranked by verification strength, evidence, and original report placement.
Traditional PHP uses a shared-nothing execution model in which every HTTP request requires an isolated worker process, unlike long-running Node.js event loops or Go goroutines.
On each request Nginx passes over a FastCGI socket to a free PHP-FPM worker, which boots framework bootstrap files, autoloaders, environment configs and database connection pools, runs application logic, queries MySQL or PostgreSQL, serializes the response, then tears down request state and frees memory.
The pm.max_children directive in php-fpm.conf defines the maximum number of concurrent workers; on an 8GB RAM server running a modern framework each worker consumes 40MB to 70MB, limiting pm.max_children to roughly 60 to 100 workers.
The post gives max throughput as worker pool size divided by average response time, worked as 100 / 0.2s = 500 req/sec for a 200ms endpoint.
Adding Redis caching inside PHP controllers reduces database query load but does not fix PHP-FPM worker exhaustion, because the worker is occupied for the entire duration of the request and framework routing, dependency injection containers, middleware pipelines and JSON serialization still run on every hit.
Even a 20ms Redis-cached response limits a 100-worker pool to 5,000 req/sec under perfect conditions, while consuming significant CPU cycles.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 31, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Build concurrency on one VPS is a division problem, and the app you serve pays the remainder1 distinct publisher
build
Stop defending numprocs=10: derive queue workers from a latency promise1 distinct publisher
build
Once the question needs a cube, you own the parser1 distinct publisher
build
The ICO fines what you cannot prove: Article 32 makes encryption and erasure an engineering liability1 distinct publisher
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.
One self-published post, two very different grades of number
Everything traces to a single dev.to post, but the numbers inside it are not equally trustworthy. The pool arithmetic — 100 workers, 200ms, 500 req/sec — and the 40–70MB-per-worker band are checkable on paper against standard PHP-FPM behaviour. The 12ms TTFB and 95% offload arrive with no workload, no hit ratio, no test rig and no second observer; they sit in the same paragraph as the maths and borrow its credibility.
No user, no deployment, no traffic
Nothing here tells us anyone runs this. There is a purge endpoint printed in a code sample and a header namespace — no customer, no deployment, no disclosed traffic volume, not even an anonymous case study. We would rather record nothing than count a curl call as usage.
dev.to's own numbers put out the fire it sells against
Follow dev.to's figures rather than its framing. If 20ms cached responses really let 100 workers do 5,000 req/sec, then the 1,500 req/sec spike used to stage the collapse is a third of the ceiling; blend the load 90/10 at 20ms and 200ms and roughly 57 of 100 workers are busy. An edge tier can still be the right purchase for global latency and origin spend — but it is being sold against a fire already put out by the cheap fix the same post dismisses.
Vendor-neutral diagnosis, single-vendor prescription
The fix has a brand, a namespaced header, an API key and a hosted endpoint at api.getapexcache.com, and the purge sample is effectively client-library documentation. The first half would stand in any PHP performance guide; the second half admits exactly one answer, and no relationship to that answer is declared anywhere in it.
Sure about the queueing, agnostic about the box
The split is easy to make and that is why we can score it: worker-per-request occupancy is decades-old, publicly documented behaviour, so the mechanism half of this story needs no corroboration. What we cannot judge from one post is whether coalescing holds 499 requests correctly under real concurrency, or what a global tag purge does on a bad day — and no adoption signal exists to help us decide.