(Spine Ch. 25.)
““Making processors faster is increasingly difficult,” John thought, “but maybe people won’t notice if I give them more processors.”” James Mickens, The Slow Winter, USENIX ;login: (2013)
Production went into launch week with five PHP-FPM workers. Five. Not five hundred, not fifty, the number that ships in the default config file, sitting there since the day the box was built because nothing had ever pushed on it hard enough to ask. The fifth concurrent request got a worker. The sixth got a queue and a timeout. Four days later we ran a load test specifically to saturate the job queue and watched queue depth sit at zero for the entire run, which meant the thing we had spent those four days tuning was not the thing that was broken, and had never been.
Bottom line: Fan out only the work that is actually independent. Almost none of it is. Your five agents are not five agents, they are five clients of the same database, the same rate limit, the same connection pool, the same port, and the same retry timer. The moment the shared thing pushes back, all five back off together, all five retry together, and you get a failure that looks like a model problem and is actually a plumbing problem. Parallelism is free in the prompt and expensive in the infrastructure, and the bill always arrives at the narrowest shared resource.
Host 'x' is blocked because of many connection errors. Unblock with 'mysqladmin flush-hosts'.
You retry. Retrying is what caused it.npm run dev and the second one dies
on port 3000, or worse, silently attaches to the first one’s server and
reports that its change is live.Render, July 2026: parallel branches “are not independent in the ways that matter. They share a rate limit, a connection pool, and a retry schedule. When the shared resource pushes back, every branch backs off together and then retries together.”
The shared resource isn’t slow; parallel agents built from the same code share backoff constants, so contention turns your fan-out into a synchronized drum circle hammering the same endpoint at the same millisecond: not a distributed system, the Hampster Dance (1998) with a cloud bill.
Render publishes no numbers with that, and as far as I can find nobody else has either: no measured curve of throughput against fan-out width, no published N where more agents starts costing you. Treat every width you hear as folklore, mine included.
The shared things, in rough order of how often they have bitten me:
Connection pools. Webalert, June 2026: “There are really two limits in play… the application pool size (how many connections your app will open) and the database’s max connections… Exhaustion happens when you hit whichever is smaller.” Four boxes at 40 workers each is 160 connections asking a database configured for 151.
Worker pools. InMotion, April 2026, on PHP-FPM: the max-children error appears “when the pool has exhausted all available child processes.” Size it as available RAM for PHP divided by average memory per process.
Rate limits. Shared across every subagent using the same key. Your fan-out width is the multiplier.
Ports and files. The dumbest and the most common. Claude Code’s own docs are blunt about the fix: “Worktrees give each session a separate git checkout, so parallel sessions never edit the same files.”
Separate checkouts are not always enough, which I learned on
commander-in-chief on 25 July 2026. Six agents, six worktrees, and the
test gate kept failing on clean diffs: “no log carried this run’s
marker,” 3 of 6 agents in one afternoon. Godot’s user:// is
keyed on the project NAME, not the checkout path, so all six worktrees
wrote into the same directory, and a sibling Godot kept rotating the log
away before the gate could read its own run back. The save and settings
suites were stomping the same real config file. Fix was a fresh temp
HOME per run that fails closed if the redirect didn’t take, and then two
concurrent full suites both passed, 800 methods and 17,859 assertions
apiece. Six isolated checkouts, one shared directory, and I had been
calling that isolated for weeks.
A unit of parallel work must own everything it writes. Its own checkout, its own port, its own database or schema, its own rate-limit budget. If two units share a writable thing, they are not parallel, they are interleaved, and interleaved without a lock is a race you have not lost yet.
airank, 12 August 2026. Launch week.
Five. The council measured actual memory at 60.5 MB per worker, and nobody had changed the number because nothing had pushed on it. “Nobody” is me. I looked straight at a 5 where a 40 belonged. Tuned to 40 workers with 500-request recycling, and shipped Monday on FPM without Octane, because Octane was a bigger bet than launch week could absorb (route:cache bugs, HTMLPurifier safety, an audit nobody had time for).
Four days later, 16 August 2026, the load test to saturate the job queue found queue depth at zero for the whole run: the web tier was the bottleneck, and always had been.
The autoscaling behaved: 8 workers up to 40, distributed evenly across four boxes, 10-10-10-10. Jeremy Schoemaker, twenty-five years of shipping software, spent four days getting a number from 5 to 40 with a stopwatch and a spreadsheet.
The actual measurement: Octane on FrankenPHP against PHP-FPM on identical hardware, p50 latency 1068ms down to 48ms, a 22x improvement that had nothing to do with the workers. (Deploynix measured 2.5 to 3.1x on throughput in April 2026: that is the number to expect. My 22x is p50 on one app.)
The part worth stealing: the Octane cutover broke four separate things, all silent. SSL config. The XML sitemap. Mixed content from trusted proxies. And deploy ownership, because the FrankenPHP worker file was gitignored. The single file that mounts the entire app in prod, and I told git to pretend it did not exist, then wondered why the deploy came out limp and did not finish. All four found by looking, not alerting.
19 August 2026 put the bow on it. Pricing a third web zone at $16.26/month surfaced that the database was single-AZ, which made all three web zones decorative. You cannot parallelize past a single point of failure; you can only make it more expensive to be wrong about. Multi-AZ database was $99.92/month, 6x the web zone. Both shipped, $116.18/month total, about 1% of daily inference spend.
My own host block, 7 August 2026, was dumber than any retry storm.
The health command answered
SQLSTATE[HY000] [1129] Host '192.168.1.33' is blocked because of many connection errors,
and every section under it went dark at once: workers unqueryable,
collection recency unqueryable. I was sure something was fanning out
with bad credentials. Nothing was wrong with the credentials.
max_connect_errors sat at its default of 100 and
skip_name_resolve was OFF, also the default, which means
MariaDB does a reverse-DNS lookup on the client IP for every single
connection. My LAN has no PTR records, so every failed lookup counted
toward the 100 and a busy app host locks itself out of its own database
in ordinary operation. The security feature was eating the application.
The fix is skip_name_resolve = ON, not a bigger bucket
under the same leak, and you check
SELECT user, host FROM mysql.user before restarting,
because with name resolution off any grant written against a hostname
never resolves again. I checked first. Every grant was IP-based. That is
the only decision from that morning I get credit for. Later I audited
about 2,300 error signals across my own fleet, and connection storms
against a blocked host were the single largest bucket, roughly 270 of
them. That is a polite way of saying the most common database outage I
have is me knocking harder.
The loud failure is the crash: port in use, pool exhausted, host blocked. It’s a stack trace with a name on it.
Parallel agents succeed individually and produce a wrong result collectively.
Each subagent reports done, and each is telling the truth about its own work. Agent 3 edited the file; agent 7 edited the same file from a stale read; the merge kept whichever landed last. The work is gone and the transcript says success eight times. Ch. 27 is the entire chapter on cleaning this up.
Second quiet failure: you tune the thing you fanned out instead of the thing they share. Four days on worker counts when the answer was a 22x latency change in the runtime underneath them. Parallelism makes the busy part visible and the shared part invisible, where the ceiling lives.
Third: the retry loop that digs the hole. MariaDB’s
host-block counter increments on connection errors. AWS documents the
same thing on Aurora, October 2022: reconnecting clients “generates a
connection storm,” and recovery requires
mysqladmin flush-hosts on the server. Once, not in a loop.
It is the AOL busy signal (1996) all over again: redialing faster never
once got you online, it just kept the line occupied for everybody, and
at least in 1996 the modem had the decency to scream at you about
it.
Do
mysqladmin flush-hosts once, then reconnect with
backoff.Don’t
Ch. 24 decomposed the work into units. This chapter asks which of those units can run on the same clock: fewer than you want. Ch. 26 is the coordination layer for the ones that can’t be independent. Ch. 27 is the cleanup, merging parallel output back into one tree, where the silent overwrite from this chapter’s quiet failure finally becomes visible. Ch. 59 is this chapter at scale: swarms, where the fan-out is wide enough that the shared-resource math stops being an optimization and becomes the architecture.
Thesis is Jeremy’s (independent work same clock, shared infra lies to you): argument, not citation.
Verified:
Parallel agents sharing rate limits, connection pools, and retry schedules cause synchronized contention. Render, July 2026: https://render.com/blog/infrastructure-patterns-for-agentic-applications
Connection pool exhaustion occurs at the smaller of application pool size or database max_connections. Webalert, June 2026: https://web-alert.io/blog/database-connection-pool-exhaustion-too-many-connections-guide
PHP-FPM worker pool exhaustion and the sizing formula (available RAM ÷ average memory per process; typical worker 30-150 MB). InMotion Hosting, April 2026: https://www.inmotionhosting.com/support/server/php-fpm/fix-max-children-error/
Laravel Octane delivers 2.5-3.1x higher throughput than PHP-FPM, at higher per-worker memory. Deploynix, April 2026: https://deploynix.io/blog/php-fpm-vs-laravel-octane-on-deploynix-real-world-performance-comparison
Worktree isolation for parallel Claude Code sessions. Claude Code Docs, 2026: https://code.claude.com/docs/en/agents
Connection storms and the
Host 'host_name' is blocked error class. AWS Prescriptive
Guidance, October 2022:
https://docs.aws.amazon.com/pdfs/prescriptive-guidance/latest/hyperscale-aurora-mysql/hyperscale-aurora-mysql.pdf
MariaDB connection troubleshooting. MariaDB Docs, August 2026: https://mariadb.com/docs/server/mariadb-quickstart-guides/mariadb-connection-troubleshooting-guide
Worked example 1: five php-fpm workers at launch, 60.5 MB
measured per worker, tuned to 40 with 500-request recycling, Octane
deferred. airank, 12 August 2026:
~/Projects/airank/blog/2026-08-12-five-workers-and-a-jury.md
Worked example 2: load test found the web tier, not the queue
(queue depth zero); Octane/FrankenPHP p50 1068ms → 48ms; autoscale 8→40
across 4 boxes; four silent breakages (SSL, XML sitemap, trusted
proxies, gitignored worker file). airank, 16 August 2026:
~/Projects/airank/blog/2026-08-16-the-bottleneck-was-never-the-workers.md
Worked example 3: third web zone $16.26/month vs Multi-AZ
database $99.92/month, total $116.18 (1% of daily inference spend);
three false signals (grep substring “unhealthy” contains “healthy”, cold
ALB DNS node, null SecondaryAvailabilityZone 5.5 minutes
post-conversion). airank, 19 August 2026:
~/Projects/airank/blog/2026-08-19-three-web-zones-one-database.md
Worked example 4: parallel Godot suites collided on
user://, keyed on project name not checkout path; “no log
carried this run’s marker” on clean diffs, 3 of 6 agents in one
afternoon; save/settings suites stomping one real config file; fixed
with a fresh temp HOME per run that fails closed; 2 concurrent full
suites both PASS, 800 methods / 17,859 assertions. commander-in-chief,
25 July 2026: commit 67a791a
Host block:
SQLSTATE[HY000] [1129] Host '192.168.1.33' is blocked because of many connection errors,
from max_connect_errors at its default of 100 plus
skip_name_resolve OFF on a LAN with no PTR records; fix is
skip_name_resolve = ON after auditing
mysql.user for hostname grants. airank, 7 August 2026:
~/Projects/airank/blog/2026-08-07-three-alarms-none-of-them-wired.md
Connection-storm retries against a blocked host were the largest
bucket, roughly 270 signals of about 2,300, in Jeremy’s own error audit:
~/.claude/CLAUDE.md (Error Prevention section)
What I could not verify: