RAFAEL REIS case study
migration · zero downtime

Migrating 50+ live tenants across data centers, with zero downtime

TL;DR

Stack: Proxmox · PostgreSQL/Patroni · MongoDB replica sets · Redis Sentinel · PgBouncer · Cloudflare Load Balancer · GitOps control plane

The problem

Sixty-odd dealerships depend on these sites to sell: catalog, leads, e-commerce. Downtime is not a number on a dashboard — it's a customer who opened the store's site, watched it not load, and went to buy from the competition.

All of production ran in a single data center abroad — users in Brazil, servers on a different continent, and a fleet expected to double in size in short order. The migration wasn't just a change of address: it was the chance to go from one single point of failure to two geo-separated data centers with automatic failover — lower latency, real DR, room to grow.

The obvious answer — "migrate overnight on a Sunday" — didn't work, for two reasons. First, people buy cars in the middle of the night too. Second, a platform team of one can't run on heroics: if the plan requires staying up all night and hoping, the plan is wrong. I needed a zero-downtime migration in small, reversible steps, with a cheap rollback at every one of them.

The strategy: the destination gets hot before anything flips

The principle behind everything: no traffic moves until the destination is running mirror production, with data synchronized and validated.

  1. Pre-seed: the destination runs as a replica for weeks. The full stack came up in the two new DCs and spent almost two weeks "dark": PostgreSQL as a standby of the Patroni cluster (continuous streaming replication), MongoDB replica set members, a Redis replica behind Sentinel. Data flowed to the destination in real time — without a single byte written there.
  2. An identical application, by definition. Services were provisioned at the destination by the same GitOps control plane — same containers, same versioned config. There is no "I forgot to copy that file" when the deploy is declarative.
  3. Cutover in waves, from disposable to critical. First the internal platform services (analytics, dashboards). Then a canary: a single low-risk tenant, flipped on its own and validated end to end. Only then the revenue.
  4. The cutover itself: atomic by design. Every tenant hostname points by CNAME at a single anchor record — flipping one record moves 100+ hostnames at once, and the rollback is the same record flipped back, instantly. The Patroni standby promotion was coordinated with the flip, under one hard rule: zero replication lag, or abort — and Patroni itself refuses the promotion if the lag isn't zero.
  5. Gradual ramp? Considered and rejected. Serving half the traffic from each DC during the transition sounds safer — but it reopens exactly the risk the architecture had closed: two sides believing they can write. Atomic cutover with instant rollback > gradualism with split-brain risk. A conscious trade-off.
  6. The legacy: traffic now follows the database. After the dust settled, the tenants were moved behind a Cloudflare Load Balancer whose health check only returns 200 in the data center where PostgreSQL is the writable leader. Since Patroni guarantees a single leader, traffic routing follows database failover automatically — no human intervention, detection in ~1–2 min, RPO measured in seconds.
the cutover topologyinteractive: hover over the components
traffic automatic failover streaming replication · lag 0 async replica · RPO of seconds instant rollback 1 DNS record flipped back CLOUDFLARE LB health check follows the database leader SOURCE DC overseas read-only during soak DC-A · ACTIVE Brazil PG leader (Patroni) DC-B hot-standby replica
simplified, generic topology — not the real network

"Zero downtime" is proven, not declared

Everyone says they migrated "with no downtime". Almost nobody instruments the claim. Here the validation was adversarial — the goal was to prove it broke, and fail:

If you didn't measure it, you didn't migrate with zero downtime — you migrated with luck.

the cutover, replayed
HTTP samples: 400+ · non-200: 0 · zero dropped connections
cutover · 1 DNS record time →
replay of the real data: 400+ continuous samples crossing the cutover · each bar aggregates samples · green = 200 response

The incident the drill revealed

A migration in production is not the end of the project. Days after the cutover, I ran a failover drill: send 100% of production traffic to the standby — on purpose, on a good day, to prove the automatic failover could take a bad one.

It couldn't. The standby was cold — empty caches — and it melted: a single site container at 138% CPU, load 168 on a 24-core host, while the warm primary served the same traffic at a load under 10. Failback via runbook in ~9 minutes, nothing lost beyond the spike. And then, root cause analysis.

The root cause wasn't in the infrastructure — it was in the behavior of Next.js SSR with a cold cache, a self-perpetuating runaway engine: the cold render blows past an internal timeout and becomes an error; the framework only caches OK responses, so the cache never warms up; every subsequent request re-renders from scratch — and the CPU feeds its own fire. A cold environment wasn't a transient state that "passes": it was a stable trap. It's the kind of root cause you only find when you understand the runtime you operate, not just the VMs underneath it.

the meltdown, replayed
cold standby taking 100% of the traffic · load: 7 · failback via runbook in 9 min
cold SSR cache cold render blows past the internal timeout response becomes an error framework only caches OK cache stays empty the CPU feeds its own fire every request re-renders from scratch
real numbers from the postmortem: load 168 on a 24-core host · failback in ~9 min · replay curve is illustrative

The fix came on two tracks, documented in a postmortem:

Lessons

Other case studies

A one-person platform team 60+ tenants, two data centers, one engineer. By architecture, not heroics. 12 years of homelab: my self-hosted AI stack Local LLMs on a GPU, hybrid RAG, and an agent that pentests my own SaaS.

Always happy to compare notes with people who run real infrastructure.

LinkedIn