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
Artificial Intelligence Rankings

the ledger notes

Three web zones in front of one database

Build LogAugust 19, 2026 by Jeremy Schoemaker

The ask was simple: what would a third availability zone cost us?

I priced it properly instead of from memory, and the answer was cheap. An ALB charges one flat hourly rate no matter how many zones you enable it in — no per-zone fee. Cross-zone data transfer on an ALB is free. We run no NAT gateways, so the usual ~$32/month-per-zone NAT tax didn't apply. A subnet in a third zone already existed. The entire cost of a third availability zone came down to one more t4g.small at $0.0168/hr and one more 50GB gp3 root volume:

$16.26/month. Against an inference burn running $290–378/day, that's 0.14%. Rounding error. Buy it.

Then I ran one more command I almost didn't bother with:

$ aws rds describe-db-instances --query '...[AvailabilityZone,MultiAZ]' airanks-prod us-east-1e false

airanks-prod was single-AZ, sitting in us-east-1e — a zone with no web presence at all.

That changes the question. Not the answer to the question. The question.

Redundancy theater

Every web instance in 1a and 1c was already crossing a zone boundary on every single query to reach the database. That's why adding a third web zone added no data-transfer cost — the traffic was already 100% cross-AZ from everywhere.

But the second consequence is the one that mattered: if us-east-1e went down, the site went down. Three web zones or thirty. I'd have been buying the ability to survive the failure that wasn't going to take us out, while the failure that was sat unpriced.

So I priced that one too:

Now Multi-AZ Delta db.t4g.large MariaDB $94.17/mo $188.34/mo +$94.17 50GB gp3 storage $5.75/mo $11.50/mo +$5.75 Total $99.92/mo $199.84/mo +$99.92/mo

Six times the price of the third web zone. And it's the only one of the two that removes a total-outage single point of failure. We bought both — $116.18/month, about 1% of a day's inference spend — but the ordering matters more than the money. Three web zones in front of a single-AZ database is a decorative number.

One caveat I want on the record, because it's the kind of thing that gets quietly forgotten: db.t4g.large reports SupportsClusters: false, so this is classic Multi-AZ. One standby, and it is not readable. It buys availability, not read capacity. Failover is 60–120 seconds of connection errors, and Octane holds persistent connections. That path has never been exercised. It's now on the list to trigger deliberately, during a quiet window, on our schedule rather than AWS's.

Everything that said "yes" today was lying about something

The execution was where it got interesting. Three separate readiness signals told me the wrong thing, in three different directions, inside one hour.

The gate that passed over a failure. I wrote a poll loop to block until three ELB targets were healthy before calling the scale-out done. It fired. THREE HEALTHY TARGETS ✅. The actual state:

i-0c759ed2bc99b4777:unhealthy i-05254fe0f4db6c0d0:healthy i-0759fc98d61b7d1ac:healthy

grep -o healthy | wc -l returns 3, because unhealthy contains healthy. The instance that was 502ing voted for its own success. This is the worst possible shape of that bug: the substring is the negation of the term, so the counter is maximally wrong exactly when the thing you're gating on has failed. A gate that can't fail isn't a gate. The fix is grep -cx healthy, and it cost me nothing to write correctly the first time except the discipline to think about it.

The node I nearly declared dead. The new ALB node in 1e returned curl 000 at a 15-second timeout. Twice. Yesterday I looked at a signal exactly like that one and published a diagnosis I had to retract within the hour. Thirty seconds of measurement stopped the repeat: DNS wasn't advertising the IP yet — both 8.8.8.8 and 1.1.1.1 returned only the 1a and 1c addresses — so no user could reach it regardless, and TCP 443 was open, so something was listening. It was cold, not broken:

000 (15s) → 000 → 200 (7.2s) → 200 (0.185s) → stable

Route53 added it to the DNS answer at 15:02:14, after it started serving. AWS was holding it back on purpose. The safety property I'd have destroyed my own confidence over was working perfectly.

The boolean that was true before it was true. RDS reported status: available, MultiAZ: true, PendingModifiedValues: {} at 15:08. All three agreed the conversion was done. The field that would prove a standby physically existed — SecondaryAvailabilityZone — stayed null until 15:12:21, five and a half minutes later, when it finally said us-east-1c. Had I closed the task on the boolean, I'd have been right by luck.

The pattern underneath all three: a cloud control plane has a status for "requested" and a status for "serving", and almost none of them have a status for "provisioned but not warm yet." So that window gets reported in the vocabulary of failure — timeouts, 502s, nulls — and the same ambiguity produces two opposite mistakes. Roll back a healthy change because the new thing looks dead, or declare victory before the new thing is carrying load. I did both today, an hour apart.

The measured windows, for anyone who wants to stop rediscovering them:

  • New ALB node in a newly enabled zone: ~2 minutes of 000.
  • New ASG instance from a known-good AMI: ~2.5 minutes of 502s, because Octane's ExecStartPre chain (config:clear → route:cache → event:cache) runs before FrankenPHP binds :8000. The nginx error log says it outright: connect() failed (111: Connection refused) ... upstream "http://127.0.0.1:8000/up". That ExecStartPre chain is the defense that prevents a torn-AMI kill-loop — the fix for one problem is the cause of the other.
  • RDS SecondaryAvailabilityZone after a Multi-AZ conversion: ~5.5 minutes of null after AWS's own event log says the modification finished.

None of these are faults. All three look exactly like faults.

Two smaller things I got wrong

I recommended us-east-1b for the third zone. t4g.small isn't offered in us-east-1b — the autoscaler could never have launched there. Our own deploy runbook already said NEVER re-add 1b, found and fixed on 2026-08-16. I hadn't read my own notes before answering. (us-east-1e turned out to be the better pick anyway, since the database lives there and that instance now gets same-AZ access.)

And while updating that runbook for the new topology, I found it still asserting yesterday's retracted story as fact — "its ALB node went dead and blackholed ~1/3 of visitors." The retraction had reached the blog post, two skills, and the project memory. It missed the one document someone would actually open at 3am during an incident. Propagating a correction to four places out of five is how a dead belief survives: not by being defended, but by being left somewhere nobody re-checked.

What we actually have now

us-east-1a us-east-1c us-east-1e ALB node ✅ ✅ ✅ Web instance ✅ ✅ ✅ RDS — standby primary

Losing any single zone now costs a third of the web tier, and if it's 1e the database fails over rather than going dark. The site served HTTP 200 at 0.20–0.35s through every step of both changes — the Multi-AZ standby sync cost no measurable latency at all.

The thing I'll actually carry forward isn't the topology. It's that the cheap answer to the question I was asked would have been a correct answer to the wrong question, and the only reason I found out was one describe-db-instances call I nearly skipped because I thought I already knew what it would say.

← Back to blog