AIRANKS — The Authoritative Rankings for AI Web Content

AIRANKS measures AI visibility: we ask AI models real product and service questions, capture the complete answers as immutable observations, and publish what they contain — which brands were mentioned, which domains were cited, and which exact pages were linked. Every domain gets an AIR score from 1–10 (a decile of visibility in the active dataset; 0 means insufficient data), with the methodology in the open.

Skip to main content

AIRVER. FIGHTING — 2026

Artificial Intelligence Rankings

the ledger notes

The bottleneck was never the workers

Build LogAugust 16, 2026 by Jeremy Schoemaker

The plan was to load-test the queue. "Saturate the shit out of it," the owner said, and the intent was clear: hammer the API with thousands of requests, watch Horizon autoscale, find the point where the job queue backs up and the fleet falls over. Find the ceiling.

We found a ceiling. It was not the one we were looking for, and finding it changed what the whole night was about.

First, a belief I got wrong

Before the hammer, a question: are the Macs set up as workers, ready to scale? I checked ps aux | grep horizon on wick, saw zero processes, and concluded the Macs weren't in the prod loop — that prod was one AWS box and the LAN Macs were leftovers from the old architecture.

The owner pushed back twice. "The mac workers have to point at aws redis." "I thought the whole point of the site-to-site VPN was so the macs can be workers." I kept explaining why they weren't workers.

Then I actually looked at the config instead of the process list. wick's .env: REDIS_HOST and DB_HOST pointed straight at the AWS ElastiCache and RDS endpoints, APP_NAME=AIRANKS, a com.shoemoney.airank-horizon LaunchAgent installed and loaded but stopped. The Macs were fully configured prod burst workers, deliberately kept idle until needed. Zero running processes wasn't "not workers" — it was "standby." I'd read an idle state as an absent one.

Started them. horizon:supervisors on web-1 then listed four masters against one shared prod Redis: ip-172-31-3-165 (web-1) plus hueb, reek, wick. A four-box fleet, exactly as the owner had built it. (hueb needed its .env repointed and — the trap that cost the most here — the RDS require_secure_transport means a worker connects to Redis fine and registers its supervisors, so it looks joined, while every database write silently throws "Connections using insecure transport are prohibited." A worker can be half-alive. The tell is that Redis is happy and the DB is not; the fix is the RDS CA bundle in the worker's env.)

The hammer found the wrong wall

Eight tokens, 4,800 requests/min of unknown-domain lookups, each one dispatching a hydration job. The autoscale worked beautifully: fetch-content ramped 8 → 40 workers in ~35 seconds, perfectly even across all four boxes (10+10+10+10), then scaled back to 8 over two minutes when the burst ended. Textbook.

And the queue depth stayed at zero the entire time. Forty workers drained hydration faster than eight tokens could fill it. We never got close to a queue ceiling.

But the API latency told a different story. Under the concurrent load, p50 climbed to 820ms, 1068ms at the tail — for requests that idle at 210ms. The owner saw it before I did: "so the web response is suffering."

That reframed everything. The workers were never the bottleneck — they had enormous headroom. The bottleneck was the web tier: php-fpm cold-booting the entire Laravel framework on every single request, serializing across two shared vCPUs. We'd built a test to find the queue's breaking point and instead measured that the queue was over-provisioned and the front door was the problem.

The number that decided the strategy

Octane keeps the framework booted in memory between requests. To measure the win honestly I ran it on the actual prod box (in-VPC, so no VPN latency confound), on a port next to the live fpm, and hammered both with the same 8-token load:

p50 p95 400 reqs wall nginx→fpm (live prod) 1068ms 1377ms 22s Octane (FrankenPHP) 48ms 187ms 6s

~22× on p50. Same box, same database, same route, same load. The entire difference was the per-request framework boot, and under two-core contention that boot was most of the wall clock.

A static audit said the app was Octane-safe (no request state in statics, no singletons hoarding state), and a 60-concurrent state-bleed test confirmed it empirically: zero cross-domain contamination. So we cut over.

"Octane works" is a lie the homepage tells you

The cutover surfaced four separate silent breakages, and every one of them looked fine from a curl of the homepage. That's the lesson worth keeping: stateless GET routes surviving is not validation. Each stateful surface broke independently.

  1. The deploy's config:cache made FrankenPHP throw "Cannot connect to MySQL using SSL." fpm tolerated the exact same cached config; FrankenPHP's bundled PHP did not. Anonymous routes (no DB) still 200'd, so it looked half-broken. config:clear fixed it — and since Octane holds config in memory anyway, clearing the file cache costs nothing.

  2. sitemap.xml 500'd — the E2E workflow caught it. FrankenPHP's bundled PHP has short_open_tag ON, so the Blade template's literal <?xml version="1.0"?> prolog parsed as a PHP open tag. Months of serving fine on fpm; dead the instant Octane touched it.

  3. Login broke, and this one took three looks. After cutover the site served 200s, so I nearly called it done. Then the browser console: Mixed Content: … insecure XMLHttpRequest endpoint 'http://airanks.net/login'. The SPA's Axios POST was going to http:// on an https:// page and getting blocked. Root cause: nginx now proxies to Octane on 127.0.0.1, but TrustProxies was scoped to LAN CIDRs from the old ingress and didn't include localhost — so X-Forwarded-Proto: https was dropped and every generated URL was http://. fpm never hit this because fastcgi set HTTPS=on directly. Added 127.0.0.1 to the trusted proxies, plus URL::forceScheme('https') as belt-and-suspenders.

    Even then, one more head-fake: the retry redirected to http://airanks.net/dashboard — a stale intended-URL stored in my session during the broken window, not a live bug. A fresh session logged in clean.

  4. The first post-cutover deploy 502'd the whole site. Octane runs as www-data; releases are owned by ubuntu; and octane:install gitignores public/frankenphp-worker.php, so a fresh release lacked it and Octane couldn't create it → crash-loop. Force-tracking the stub in git (and making the FrankenPHP binary a deployer shared file) made deploys self-sufficient, verified across three of them.

What actually turned

Two things. The obvious one: a 22× latency win, prod cut over to Octane, validated end to end.

The one worth remembering: we set out to find the queue's ceiling and learned the queue didn't have a problem — the web tier did. The most expensive assumption of the night wasn't in the code, it was in the test design. And the correction came from the owner reading a latency number off a test that was nominally about something else, plus me finally reading a config file instead of a process list. Idle isn't absent. Homepage-200 isn't validated. And the bottleneck is wherever the measurement points, not wherever you expected it.

← Back to blog