The outage that moved us to Cloudflare

At 06:02 a Google Cloud zone stopped answering and took two products, both still in beta, with it. Our own review said stay and pay for high availability. By 15:45 we had left anyway. Here is the reasoning, and the objections to it.

06:02 and 15:45, joined by an arrow, set large on a pale grey background beside a 24-hour clock face with the stretch from 06:02, when the zone stopped answering, to 15:45, when we were off Google Cloud, drawn as a thick arc, above the words: Google Cloud fails at dawn. By mid-afternoon we have left.
06:02Postgres stops answering in europe-west2-b
230send to end for the final TipBop cutover
0rows lost, proved by a diff of every table
£12.61TipBop's whole Google Cloud bill the month before

Postgres wrote its last log line at 06:02:22. Three minutes later every Cloud SQL metric for TipBop’s production database flatlined, and every connection from the app timed out. Nobody had deployed anything. The serving revision was two days old. Google’s status page said everything was fine.

It was not just TipBop. Two TyreStack databases in a different project, same zone, went dark in the same minute. That was the first useful clue: this was europe-west2-b having a bad morning, not anything we had done.

Why it was in one zone at all

Both products were in beta. TipBop had no external customer, and TyreStack had no signed fitter. In that state we had made a deliberate call, written into TipBop’s decision log on 1 October: run the database in a single zone, on a shared-core machine, and do not pay for high availability yet. The log gave the reasons. There was no external customer to hurt. Deletion protection was on. We had drilled a restore and a point-in-time recovery clone the day before, and the clone took 32 minutes and matched production. And high availability roughly doubles the database line on the bill. We set ourselves a trigger: review by 31 December, or before the first external customer, whichever came first.

The obvious objection is that a payments app on one zone of a shared-core database is a bad look, and it would be if it had customers. Google’s own advice is not to run production on a shared-core tier, and it carries no uptime promise. We knew that. The trade was that, in beta, a day of downtime would cost us our own time with no external customer waiting, while a permanent high-availability bill bought protection for customers who did not exist yet. The outage arrived before either of our triggers did.

The same morning’s review wrote the rule down for later: a production database that holds real customer data or money is regional, on a dedicated core, from its first real customer. Shared-core and single-zone are for staging and beta only. So the argument is not that one zone is fine. It is that one zone was right for that stage, and that the stage had an exit condition written down in advance.

The morning, hour by hour

On 20 October the gap came due.

20 October 2026, in UTC

From the first dead log line to the Google project being deleted.

06:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0006:02Postgres stops answering in zone b06:42Restart. It comes back but stays unreachable07:21Backup restored into a new zone09:06Cut over. A stuck script cost 1h45m10:37Cloudflare port starts13:52Cutover attempt 1 rolls itself back14:42Cutover attempt 2. Live on Cloudflare15:45Cloud Run and Cloud SQL deleted

Source: TipBop operations log

The restart at 06:42 took twenty minutes and was a false dawn. Postgres came back but nothing could reach it, and write-ahead logging was still failing. Rather than wait an unknown number of hours for Google, we restored the 04:29 backup into a fresh instance in zone a.

That decision had a cost we could measure. Anything written between 04:29 and 06:02 was at risk. The request logs for that window showed no writes at all, only bots poking at paths that do not exist, and a full diff later proved it: every primary key and a checksum across every table, zero rows missing.

The restore finished at 07:21. We did not cut over until 09:06. The cutover script sat in a wait loop because a filter in one gcloud command matched nothing, so it waited politely for a condition that could never be true. One hour and forty-five minutes of downtime was ours, not Google’s. That script now fails loudly instead of waiting forever.

The review said no

While TipBop limped back, we asked a harder question: should we be on Google Cloud at all? We wrote an adversarial review that morning, deliberately trying to talk ourselves out of a knee-jerk move. It was good, and it said no.

Its argument went like this. The outage hit a zonal, shared-core database. Nothing about it says “change cloud”. It says “pay for high availability on the one product that takes money”, a bill of about £45 to £60 a month instead of about £12. It pointed out that Cloudflare had several outages in 2025 that would have hurt us, the longest lasting hours, and that every one of our products already sits behind Cloudflare’s DNS and proxy. Our real single point of failure was Cloudflare already.

That is a fair argument and we still think it is mostly right as written. We overruled it anyway, for three reasons.

Stay, add high availabilityWhat the review recommended

Proven code, about 60 seconds of automatic failover. But the bill for TipBop goes from about £12 to £45 to £60 a month, and we still run Postgres, Cloud Run, connection pools and cold starts.

Stay, move Postgres to NeonRejected

Cheaper than high availability, but adds three sub-processors to the privacy notice, and its compute still recovers from a zone failure in one to ten minutes.

Move to Cloudflare Workers and D1Chosen

About £0 to £4 a month. One platform we already depend on, instead of two. The catch is real: D1 has no interactive transactions, so every locked money path had to be rewritten.

First, cost at our size. TipBop’s entire Google bill the month before was £12.61. The fix the review proposed would have multiplied that by four before a single extra customer arrived. TyreStack’s projected bill at twenty customers was £150 to £250 a month on Google with high availability, against £5 to £15 on Cloudflare.

A fair challenge is that a day of work to save about £10 a month on the bill we actually had, or more than £40 against high availability, is a bad trade. If money were the only reason, it would be. It was not the main one. The effort of the port was not our constraint, so the question was which end state gave us the lowest bill, the fewest things to fail and the quickest restore. The bill was the tiebreaker, not the case.

Second, fewer moving parts. On Google we ran a container platform, a managed Postgres, a connection proxy, a storage bucket and a Cloudflare Worker in front of all of it. Each one is a thing to patch, monitor and pay for. On Cloudflare it is one Worker and one database, behind the proxy we already trusted with everything.

Third, and this is the one people underrate, restore speed beats uptime promises. The question that actually hurt that morning was not “how often does it break?” but “how fast can we put it back?”. We wanted a setup where putting it back was a script we had already run, not a support ticket.

Why it was not reckless

Moving a payment app in beta between clouds in an afternoon sounds like the kind of thing that ends up in a postmortem. Three things made it boring instead.

The old system stayed up and untouched until the new one proved itself. The production Worker was deployed “parked”, answering only a canary hostname, with its payment sweepers switched off so two systems could never pay out at once.

We replayed real traffic before trusting it. Twenty-four hours of real requests, 1,282 distinct paths, were sent to the new Worker running on a copy of production data. 1,273 came back byte-identical. The other nine differed by design, and each was written down with its reason.

And we rehearsed the undo before doing the thing. Rolling back meant reloading every table into a scratch Postgres and swapping the hostnames back. We timed it.

41sto reload all 23 tables back into Postgres during the rollback rehearsal, every table fingerprint-checked and the money totals equal. Swapping the hostnames back took about five seconds more.

The first real cutover attempt, at 13:52, rolled itself back. Five checks for alias domains like www.tipbop.com failed because they were being tested while the site was deliberately in maintenance mode. The script did exactly what it was built to do: it put the hosts back on the old system in about three seconds and stopped. We fixed the check, merged it, and ran it again. Attempt two finished at 14:42, all ten hostnames moved, 230 seconds end to end, every checksum matching.

An hour later we deleted the old Cloud Run service, the Cloud SQL instance, the container registry and the staging project. Leaving them running for the planned two weeks would have been the cautious move. But nobody was using the old system, keeping it meant paying for it and patching it, and we had a nightly off-platform backup, 30 days of point-in-time recovery on D1, and a final Postgres dump in cold storage. Caution has a running cost too.

What we changed for everyone

TipBop was first. TyreStack followed the same day, and ZooLog had been on Cloudflare since its first commit. That evening it became a house rule: every new service, database and background job goes on Cloudflare, with EU data residency set at creation on anything holding personal data. Google Cloud is now an exception we have to write down and justify, not a default.

Monthly hosting, one product, three ways

TipBop at its size in October 2026. Estimates from our hosting review except the actual.

Google Cloud, as it was (actual)£12.61Google Cloud with high availability£45 to £60Cloudflare Workers and D1£0 to £4

Source: Google Cloud billing export; estate hosting review, 20 October 2026

We kept one thing from the review that argued against us. Cloudflare is now a bigger single point of failure than it was, and we treat it that way. Every production database has a nightly export that lands outside Cloudflare, and a restore of that export is tested automatically every night. If Cloudflare has a six-hour day, we lose six hours. We do not lose the data.