Our AWS Bill Doubled Before It Halved: Anatomy of a Lift-and-Shift
The experiences, thoughts, and notes here are my own; the prose was rewritten with the help of an LLM.
Every cloud migration story I’d read before doing one had the same shape: legacy datacenter, bold move, costs drop. Then I helped move Near’s platform, six years of accumulated systems including the real-time bidding fleet, from IBM’s cloud to AWS, and learned the shape is actually a spike. Our infrastructure bill went from $180K a month to $400K a month before it came back down to $200K.
I want to walk through that curve honestly, because the spike isn’t a failure mode. It’s the price of doing the migration in the right order.
$400K ┤ ██
│ █ █
│ █ ██
│ █ ███
$180K ┤ ████ ████████ ← $200K, new steady state
└──┬────┬──────────┬─────
before migrate optimize
Lift first, think later, on purpose
The migration itself took three or four weekends. That number surprises people, until I explain what we didn’t do: we didn’t re-architect anything. No “while we’re at it, let’s containerize.” No swapping databases. We picked up a six-year-old system and put it down somewhere else, as-is, ugly parts included.
This was a deliberate bet. A migration has one honest success criterion, which is that the system behaves identically on the other side, and every architectural improvement you bundle into it makes that harder to verify. If something breaks after a pure lift-and-shift, the cloud move did it. If something breaks after a lift-and-shift-and-refactor-and-upgrade, good luck bisecting that across two variables on a production ad exchange. We ran the cutovers on weekends because the RTB business is quietest then, which gave a botched cutover the most room to be fixed before Monday’s traffic.
The corollary of lifting as-is: you also lift six years of accumulated over-provisioning, and now it’s metered.
Why the bill doubled
Datacenter capacity and cloud capacity are priced by different philosophies. In a datacenter you buy for peak, once, and the idle headroom costs nothing extra month to month, so systems accumulate headroom over the years. Nobody ever files a ticket to shrink a server. Copy that fleet onto on-demand cloud pricing and you’re suddenly paying for all of that idle capacity by the hour.
So: $400K. The first month was covered by credits, which is the only reason the graph above didn’t trigger a very different kind of executive meeting. That grace month bought us time to treat cost as an engineering project instead of an emergency.
The claw-back, in order of impact
Right-sizing a hundred-plus servers, one utilization graph at a time. This was the least clever and most valuable work. For every server we pulled actual CPU and memory utilization and asked what the workload actually needed. Machine after machine was sized for a 2012 guess about 2018 traffic. We stepped instance families down aggressively; boxes provisioned like c*.9xlarge-class hardware were doing c*.4xlarge work or less, and the utilization data made each decision easy to defend. If you can’t justify an instance size with a graph, you’re guessing.
Staging got sized like staging. Staging environments had been sized like production out of habit. We collapsed them onto shared VMs and spot instances. Spot is a terrible idea for a bidder that must answer in 100ms and a perfect fit for an environment whose worst failure is a QA engineer re-running a test. Matching the purchasing model to the workload’s criticality became a habit after this.
Elastic RTB capacity via Jenkins. We built web-setup and web-starter automation, Jenkins jobs that could provision and bootstrap an RTB node from nothing to taking traffic, across all four data centers. Before this, fleet size was a decision you revisited quarterly because changing it hurt. After, scaling the bidder fleet up for a new exchange or down for a quiet quarter was a Jenkins job. The saving wasn’t the automation itself; it was that capacity decisions became cheap to reverse, so we stopped padding them.
Reserved instances for the floor, on-demand for the waves. Once right-sizing settled and the Jenkins automation told us what our true baseline fleet was, we bought reserved instances for that floor and kept on-demand for everything bursty. You can’t do this step earlier: reserving capacity before right-sizing locks in your over-provisioning at a discount, which is a worse deal than it sounds.
The data transfer line item nobody budgets for. AWS charges for traffic between availability zones, and a chatty distributed system generates a lot of it. Cross-AZ transfer kept climbing the cost dashboard rankings. Our fix goes against well-architected orthodoxy: we consolidated the non-critical services into a single AZ. Multi-AZ is an availability feature, and availability has a price. For the bidder it was worth paying. For an internal reporting service that could tolerate an hour of AZ-outage downtime a year, we were buying insurance we didn’t need. We kept multi-AZ where an outage cost real money and dropped it where it didn’t.
$200K, and what the number hides
Steady state landed at $200K a month, above the old IBM bill, and that’s fine, because we weren’t buying the same thing. The $180K bought a fixed fleet that took a quarter to change. The $200K bought a fleet we could resize in an afternoon, a staging environment priced for what it actually did, and a cost dashboard that surfaced the next optimization instead of hiding it.
The part I’d underline for anyone about to do this: the claw-back was unglamorous teamwork. Dozens of people staring at utilization graphs, arguing about which services deserved multi-AZ, babysitting weekend cutovers. There is no single trick in this post. What mattered was the sequence: lift as-is, measure everything, then optimize with data you could only get by being there. Teams that invert it and optimize on paper before migrating end up defending guesses. We only ever had to defend graphs.