Zero Migration Code: Splitting Airbnb's Main Database in Two Weeks
Heading into a summer where peak traffic grew 3.5x a year, Airbnb's main database was running out of road. It was the original monolith's database, still holding much of the core data, and one feature dominated it: the message inbox drove a third of all writes, growing linearly with traffic. A mid-project discovery sharpened the deadline: on Amazon's hosted MySQL (RDS), the busier the database got, the more its own daily backups threatened to take the whole site down. The classic fix, moving a feature's tables onto their own database, normally costs weeks of migration code and consistency testing. Airbnb instead let MySQL's built-in replication do the hard part. They created a live copy of the database and removed every query that joins inbox tables to others. Then they sent writes to the copy before it was in charge, so the writes failed on purpose, waited for the copy to catch up, and promoted it. Two weeks end to end, seven and a half minutes of message downtime, zero migration code, and a third of the main database's writes gone.
Run the promotion yourself: eliminate the joins (or skip it and see what breaks), stop the writes, verify the copy has caught up, then promote, watch the downtime clock, and see what the abort button really costs.
Problem
By 2015 Airbnb's infrastructure had a stated philosophy. Like their peers at Asana and the MySQL experts at Percona, the team saw splitting one table's rows across many databases (horizontal sharding) as bitter medicine. They preferred to move whole features onto their own database instead (vertical partitioning), one database per independent service. But that was the goal, not yet the reality. Much of the core application data still lived in the original database from the days when Airbnb was one big Rails application, and peak traffic was growing 3.5x per year with the summer season approaching.
An in-house query profiler (built in-house because RDS, Amazon's hosted database, allows nothing deeper) put a number on the pressure. The message inbox, where guests and hosts communicate, accounted for nearly one third of all writes on the main database, a share that grew linearly with traffic. Those queries were projected to grow another 50% within months, which would certainly have overwhelmed the main database. Because the inbox is an independent feature whose cross-table joins could be removed, it was the obvious candidate to move.
Two realities framed the how. Airbnb hadn't partitioned a database since 2012, so doing it at 2015's scale was new territory; and of roughly 130 engineers, only a small fraction worked on infrastructure, the rest spread across search, payments, trust and safety, and mobile. The team was explicitly willing to accept some planned downtime in exchange for less engineering complexity. Then, mid-project, a discovery reframed the urgency. Under heavy load, a routine daily backup could slow the database enough to bring it down. Airbnb even ran Multi-AZ, a setup that keeps a spare copy of the database in another data center and takes the backup from that spare instead of the live one, and the latency still spiked. The team had always known snapshots raised latency; they had not known that rising load could turn a daily backup into an outage. The monolith wasn't just a capacity ceiling; it was a shared risk whose own safety machinery grew more dangerous as it grew busier.
Solution
The conventional plan (application-layer changes, migration code, bookkeeping, and consistency testing) costs weeks. One engineer proposed letting MySQL replication do the hard part instead. Airbnb ran on RDS, where creating read replicas (live copies of a database) and promoting one to a standalone master is built-in (AWS lists exactly this as a use case of Read Replica Promotion). By accepting a brief, deliberately contained downtime during the promotion, the team could guarantee the data stayed consistent without writing a single line of bookkeeping or migration code. A site-wide outage from an overwhelmed database would cost far more than a controlled message-inbox outage, so the trade was accepted.
The setup: a new copy of the main database (called message-master) that would become the independent master, plus a second copy (message-replica) attached to serve as its replica once it was promoted. The catch that shapes the whole operation: an RDS promotion takes several minutes, and during that window writes to the affected tables must be failed on purpose to keep the data consistent.
Phase one took most of the two weeks, and it was the unglamorous part: making the split real in the application's code before splitting the actual database. Because a promotion cannot be undone, every query that joined inbox tables to other tables had to be found and either removed or rewritten to join in the application instead. The internal query analyzer surfaced most of them, and revoking the database permissions flushed out the rest, turning 'we think we found them all' into enforcement. Data pipelines were repointed to read from message-replica so downstream analytics and services would read the right data after the split. Read traffic was moved to the replica ahead of time, leaving only the main app's writes for the operation itself.
The operation itself ran as a rehearsed sequence:
- Tell customer service about the sub-10-minute downtime window, run at the lowest-traffic hour of the week (a guest stranded at check-in is the real cost of downtime).
- Deploy the new database grants and connections, with writes still pointed at the main database.
- Swap all inbox writes to the unpromoted message-master through Zookeeper (a service that tracks where each database lives). The writes now fail by design, and the downtime clock starts. Reads still work, but marking a message read is a write, so messaging is effectively down.
- Kill the inbox connections on the main database directly, rather than redeploying, to stop its writes as fast as possible, since replication can only catch up once the writes stop.
- Verify the copy has caught up, three ways: the newest rows match on both sides, the old connections are gone, and new connections are arriving.
- Promote message-master. Reads are down about 30 seconds; writes are down nearly four minutes, about 3.5 of them just waiting for the promotion to take effect.
- Turn on Multi-AZ for the new master before the next backup window.
- Only once the metrics settle, drop each database's leftover tables, so no service quietly reads stale data.
The abort path was honest about its cost. Reverting the Zookeeper entries would restore messaging almost immediately, but any writes that had already landed on the now-independent database would be lost. Recovering them was possible in theory but messy and confusing in practice, which is why every step was tested thoroughly in advance.
End to end: two weeks, seven and a half minutes of message-inbox downtime, the main database 20% smaller, and its write load down 33%, removing queries that had been months from overwhelming it. The project bought time for longer-term scalability work, which is exactly what a vertical partition is for.
Tradeoffs
- Planned downtime bought engineering time at a rate the team could name: weeks of migration code, bookkeeping, and consistency testing traded for seven and a half minutes of localized messaging downtime. The trade is only worth it because the blast radius was contained by design: a controlled inbox outage at the weekly traffic trough versus a site-wide collapse from an overwhelmed database. Downtime aversion as an absolute rule would have cost weeks; downtime as a scoped, communicated, rehearsed line item cost minutes.
- Using replication to do the migration hands the hardest part, keeping the data consistent, to machinery the team already trusted in production. The price is flexibility. There is no dual-write window, no verification scripts, no catch-up code; there is also no gradual cutover, no dark reads, and no way back after promotion. Compare Notion, whose strained monolith couldn't afford the standard tooling and had to build a bespoke audit-log pipeline: Airbnb's move works precisely because they acted while the built-in tool still had headroom to run. The earlier you move, the cheaper the machinery you're allowed to use.
- The promotion is irreversible, so all the risk migrates into preparation. Every cross-table join had to be found before the op: the analyzer found most, and revoking grants converted 'we think we found them all' into enforcement. The abort path existed but leaked: reverting Zookeeper restores service while abandoning any writes accepted by the new master. When the tool makes rollback impossible, rehearsal and verification stop being diligence and become the design.
- Splitting the database feature by feature uses up a limited supply: features self-contained enough to move, whose cross-table joins can actually be removed. The message inbox was ideal: one-third of writes, cleanly separable. Each such split leaves the monolith's remainder more entangled on average, which is why the post frames the win as buying time for longer-term stability investments, not as the destination. GitHub's version of this same ceiling, a decade of schema domains and linters later, shows what the long game costs when the easy separations are gone.
- The two-week figure hides how much work it actually held: the post is candid that identifying and eliminating cross-table joins was 'the most time consuming phase', a well-communicated cross-team effort across services owned by scattered teams. The operation was an afternoon; the socio-technical work of making the data separable was the project. The post states a design principle, 'services should own their own data,' precisely because not following it is what made phase one slow.
- The RDS-snapshot discovery is the tradeoff nobody chose: managed convenience means the platform's safety operations run on the platform's schedule against your load curve. Multi-AZ moves backups to the standby, and latency still spiked nonlinearly under load: a core dependability feature that became a threat in proportion to the database's busyness. The lesson generalizes beyond RDS: shared-fate domains include the vendor machinery attached to them, and load growth can convert routine operations into incidents before capacity itself runs out.
Patterns in this article
- Replica-Promotion Split
This is the post's central move, and AWS lists it as an explicit use case of RDS Read Replica Promotion. Build a replica chain (message-master with its own second-tier replica), move reads and data pipelines over ahead of time, stop the writes, and verify the copy has fully caught up before promoting it. Battle-tested replication carries the data consistency, and planned downtime replaces migration code entirely.
- Logical–Physical Migration Split
This is the same pattern a third company reached from a different direction. Airbnb's phase one proves the separation is true in the code before any data moves. Every cross-table join is removed or brought into the application, database permissions are revoked to turn discovery into enforcement, and pipelines are repointed, all before the irreversible promotion. Figma rehearsed the new behavior behind views and flags; GitHub enforced virtual schema domains for years; Airbnb compressed the same idea into a two-week preflight.
Also solving this
Other systems in behindscale's Single-cluster scaling ceiling class: