Zephiel API
Engineering20 February 20249 min read

We rewrote the gateway and nobody noticed

Fourteen months, no maintenance window, no breaking change, and one incident that lasted ninety seconds. The strategy was refusing to do it all at once.

The gateway that routed requests for nine years was replaced last month. The work took fourteen months. There was no migration announcement because there was nothing for customers to migrate.

Why

The original gateway was written when we had ten APIs and assumptions to match: one region, one pricing model, one key per account, request logs kept forever. Every one of those assumptions had been violated by reality and worked around in place. Rate limiting alone had four code paths that were supposed to be equivalent and were not.

It was not slow or unreliable. It was becoming difficult to change safely, which is the failure mode that gets you eventually.

What we refused to do

We refused to build the new one alongside and cut over. Big-bang rewrites fail in a specific way: the new system is finished according to the plan and then discovers, in production, the six years of undocumented behaviour the old one had accumulated. Some of that behaviour is bugs customers now depend on.

Instead we put the new gateway in front as a pass-through that did nothing, then moved one responsibility at a time behind it: routing, then auth, then rate limiting, then metering, then logging. Each move was independently reversible.

Shadow traffic

Every stage ran in shadow first. The new component processed a copy of live traffic, its output was compared against the old one, and differences were logged rather than served. We required a week of zero unexplained differences before switching.

That week caught things a test suite never would have. Auth had a case-insensitivity quirk in header parsing that had been there since 2016 and that a handful of customers depended on. Rate limiting had an off-by-one in window boundaries that made limits fractionally more generous than documented, and correcting it would have broken two accounts sitting exactly at the edge. We kept both behaviours and documented them.

The ninety seconds

One incident. Metering cut over cleanly, then a counter reset in the wrong order on a deploy and about a minute and a half of usage went uncounted for some accounts. We noticed from the shadow comparison, replayed from the request log, and nobody was misbilled.

That is the entire customer-visible impact of the project.

What we would do differently

Start the shadow comparison earlier. We built three components before adding shadowing, and had to retrofit it. Everything after that was faster, because we stopped guessing whether a change was safe and started measuring it.

Keep reading