Migrating a national news site to a headless CMS with a 30-second cutover

A flagship media site moved off a decade-old in-house CMS onto Storyblok with launch day as the cutover. A load test built from real traffic, a canary on the existing CDN, and a rehearsed switch turned a five-to-fifteen-minute outage estimate into under 30 seconds.

Industry: Media and events
Company size: 200-500 employees
Services: Platform engineering, migrations, load testing, CDN and edge


The Challenge

The client was moving its flagship news site off a legacy in-house CMS, a PHP application that had accreted over a decade, onto a new headless-CMS build on Storyblok running on the Kubernetes platform we had built for them. The site already sat behind a CDN, so the new build had to slot in behind the same front door and inherit the old site's traffic patterns: an email blast pushes traffic to about 423 requests per second within minutes, and scanners send storms of HEAD requests that the legacy application rendered as full pages. The old site cached about half its traffic at the edge and had survived the rest by over-provisioning.

Launch day was the cutover. There was no soft launch on a new domain: the switch moved the production hostname from the legacy origin to the new one, touching DNS, TLS, and the CDN distribution at once, and the working estimate for the outage was five to fifteen minutes.

Our Approach

We refused to cut over until the new site had proven itself against traffic worse than the real thing, and then we made the switch itself as small as possible.

  • A load-test harness built from real traffic. We sampled 5,000 real requests from the legacy CDN logs to get the actual mix (articles 40 percent, market pages 18 percent, tags 8 percent, search 6 percent, homepage 5 percent) and ran k6 from dedicated nodes on public subnets, after the shared NAT gateway's connection-tracking ceiling capped the first attempts.
  • Answer scanner HEAD requests at the web tier. About 80 percent of a cold stampede turned out to be HEAD requests the app was rendering as full GETs. Short-circuiting them at the proxy took cold-cache p99 from 17.5 seconds to 53 milliseconds, and the origin stayed at its six-pod floor instead of scaling to its maximum.
  • Stop fragmenting the cache. Cacheable anonymous responses were setting a session cookie, so real cookie-carrying traffic bypassed the edge. Stripping it lifted offload from 70 percent to 96 to 98 percent under test.
  • Autoscaling and probes that don't cascade. Request-rate autoscaling through KEDA alongside CPU, and a readiness probe tolerant of transient saturation so a spike could not take healthy pods out of rotation.
  • A weighted canary with a one-step rollback. Because the CDN was already in front of the legacy site, we could use weighted origin routing with session stickiness to send 1, then 5, then 15 percent of real production readers to the new build before launch, with rollback being a single DNS re-association.
  • Rehearsed cutover mechanics. On the morning of the switch we profiled 18,000 real legacy requests and found that a third of the top 250 paths by weight would 404 on the new site, mostly RSS and trailing-slash variants, so those decisions were made before the switch rather than discovered after. We measured CDN configuration propagation at 167 seconds and moved the slow half of the change off the critical path. We also caught that the planned promotion targeted the wrong distribution, which would have left production configuration outside infrastructure-as-code permanently.

What the tests showed

The capacity ramp ran at 423, 600, 800, and 1,000 requests per second at the edge. At 1,000, which is 2.4 times the real blast, availability was 100 percent, p99 was 61 milliseconds, and the origin was using 11 of 20 possible pods at about 45 percent CPU. The ceiling was never reached.

The realistic test ran the real traffic mix at twice the legacy peak for 20 minutes, 50,401 requests, and we fired a rolling restart of every origin pod in the middle of it. Availability stayed at 100 percent, six pods stayed available throughout, p50 was 5 milliseconds, p99 was 70, and the deploy under load registered as nothing at all.

The Results

< 30 scutover outage, against a 5 to 15 minute estimate
1,000 req/ssustained at 100 percent availability, 2.4× the real peak
330×p99 improvement on cold scanner storms
0errors during a full rolling restart at 2× peak load
  • Cut over on a Saturday morning with every key path verified and legacy passthroughs intact within minutes
  • Day-one incidents were handled by the platform, not by people: a scanner that crawled a listing to page 69 was stopped by the rate-limit rule, and a pod-flapping bug caused by a vertical autoscaler rewriting CPU requests under the horizontal one was root-caused and fixed by policy the same afternoon
  • Out-of-range listing pages return edge-cached 404s as of the first patch release, shipped the same day
  • An honest number: the 97 percent cache-hit rate from the synthetic test was an artifact of its 350-URL list. Real traffic settles around 52 percent, the same as the legacy CMS served on the same readers. The capacity margin comes from the origin, not from the cache.

Key Takeaway

Launch when the numbers say so, not when the calendar does. A load test built from real logs, a canary on the CDN you already have, and a rehearsed switch turned a fifteen-minute outage estimate into thirty seconds, and the surprises on day one were ones the platform already knew how to absorb.

Related guide: Zero-downtime website cutover: the checklist we use


Replatforming a high-traffic site?

We'll build the load test from your real traffic and tell you whether the new stack is ready before launch day.

Schedule a call