The Largest Shard Money Could Buy: Slack's Road to Vitess
Slack stored all its data in MySQL, split across shards by workspace (one team's Slack): every team's data lived on one shard, thousands of teams per shard, with the main application handling all the routing. The model was easy to understand and easy to grow, just add more shards. Then came the question the post puts in bold: what if one team's data doesn't fit the largest shard? By fall 2016 it didn't. The biggest customers' shards had reached the largest hardware money could buy, a few shards ran hot beside a mostly idle long tail, and the scheme could not spread one big customer's load. The fix took three years. Slack moved to Vitess, a system that shards MySQL. It migrated feature by feature, double-writing to both the old and new stores and comparing the results before each cutover. By the end, 99% of its traffic (2.3 million queries per second at peak) ran sharded by finer keys like channel id (a single Slack channel), and the application no longer knew where anything lived.
Grow a whale customer on a workspace-sharded fleet and watch their shard burn while the long tail idles. Buy bigger hardware until the store runs out, try splitting a shard the scheme won't split, and prototype app-layer sharding only to hit the same wall one layer up. Then migrate to Vitess stage by stage, reshard by channel id, and watch the whale dissolve across the fleet in time for a 50%-in-one-week surge.
Problem
The original architecture is laid out with unusual completeness: three families of MySQL clusters.
- Shards held virtually all customer data (messages, channels, DMs), split by workspace id, thousands of workspaces per shard, all of a workspace's data on one shard so the application talked to exactly one database.
- A metadata cluster mapped each workspace id to its shard id, and was checked first on every access.
- A kitchen-sink cluster held data tied to no workspace, like the app directory.
Every shard was at least two MySQL instances in different data centers, active-active (both sides take reads and writes, copying to each other in the background), a hash choosing the preferred side and the other absorbing retries on failure. The webapp, Slack's single big application, owned all of it: the metadata lookup, the connection routing, the sharding logic.
The post is fair to this design before it buries it: high availability from the dual-write pair, fast product work from the everything-in-one-place model, minutes-to-diagnosis debugging, and easy growth (more teams, more shards). Then the bolded question: what if a single team and all of their Slack data doesn't fit our largest shard?
By fall 2016, with hundreds of thousands of MySQL queries per second across thousands of sharded hosts, the disadvantages had names.
- Scale limits: the largest customers' shards had reached the largest available hardware, regularly maxing out what a single host could take.
- One data model: Enterprise Grid (which spans many workspaces) and what became Slack Connect (which shares channels across companies) broke the assumption that a team's data lives on one shard, adding complexity and, in cases, a speed penalty.
- Hot spots beside waste: unable to spread a big customer's load, Slack ran a few burning shards next to a mostly idle long tail, overprovisioned because splitting shards and moving teams was hard and usage was unpredictable.
- Availability coupling: every core feature needed the team's shard up, so one shard's outage was a full Slack outage for every customer on it.
- Operations: the active-active setup was nonstandard MySQL that needed a lot of custom tooling, and with no replicas in the layout and the app routing straight to hosts, read replicas could not be used safely at all.
These are the five walls the migration set out to remove.
Solution
The choice was evolve in place or replace. The in-place option was to teach the webapp's existing routing layer to use a finer shard key like channel id (one Slack channel) instead of the whole workspace. It was prototyped seriously and rejected on the evidence: the application logic and the storage layout were deeply tangled (fetching a channel's message count assumed which team's shard held it; multi-workspace organizations were handled by checking several shards by hand). Untangling would be slow, and the approach fixed neither operations nor replica use, with the post's long-term verdict being that a surprisingly hot single-team shard still would not be easy to scale. One constraint shaped everything else: stay on MySQL. Thousands of distinct queries, some using MySQL-only features, plus years of operational practice (deployment, durability, backups, data-warehouse export, compliance), ruled out the alternatives. Non-relational stores like DynamoDB or Cassandra and newer distributed-SQL systems like Spanner or CockroachDB would all have been far more disruptive changes for a deliberately conservative infrastructure culture.
Vitess fit the requirements as built:
- MySQL Core: MySQL stays the real storage and replication engine underneath.
- Sharding: the sharding lives in the database layer, flexible and growable without adding application logic.
- Operability: automatic primary failovers and backups come built in, and a lock server tracks the layout (the topology) so the application can ignore where anything lives.
- Extensibility: it is open-source Go that Slack could extend, which it did, becoming one of the project's biggest contributors (topology metadata refactored for isolation regions, MySQL gaps closed, migration and load-testing tools, and Prometheus, Orchestrator, and xtrabackup integrations).
The migration was staged with the care of a system that cannot stop running. The first end-to-end use case in production was deliberately small, RSS feeds into channels, chosen because it forced the real work into existence: provisioning, service discovery, backup and restore, topology management (tracking which servers hold what), and credentials. Then a generic backfill system cloned existing tables while the application wrote to both old and new (double-writes), and a parallel system read from both and compared the results (double-read diffing), proving the Vitess-backed tables behaved identically before any cutover. Three years later: 99% of all MySQL traffic on Vitess, with the last 1% scheduled within two months. At peak it served 2.3 million queries per second (2 million reads, 300 thousand writes), with 2ms typical latency and 11ms for the slowest 1%.
The payoff is the changed key. Keyspaces (Vitess's name for a logical group of data that shards together) now scale roughly by users, teams, and channels. Message data is sharded by channel id rather than workspace, so the largest customer's load spreads across the fleet instead of piling onto one host.
The post's own farewell is 'say goodbye to only sharding by team, and to team hot-spots.' The proof arrived unscheduled. In March 2020, COVID moved the world's work into Slack and query rates rose 50% in one week, absorbed by splitting the busiest keyspace horizontally with Vitess's splitting workflows. By the post's own accounting, the old architecture would have been unable to scale at all for the largest customers, leading to downtime. The same foundation then carried features the migration never anticipated: Slack Connect's cross-team data, and international data residency running Vitess clusters in six regions.
Tradeoffs
- Sharding by tenant (one customer per slice of the fleet) scales with the number of customers, but silently caps how big any one customer can get. Provisioning a new shard per batch of new teams kept growth easy for years, while the largest single customer marched toward the largest available hardware, a ceiling invisible in aggregate metrics because the fleet average stayed low. The class lesson one level down: any 'everything for X on one host' scheme makes the biggest X the platform's scaling frontier.
- The intuitive data model is borrowed developer velocity, repaid at re-architecture time. 'All of a team's data in one place' made features easy to design and easy to debug. But many query sites quietly took a dependency on that locality, which is exactly what made the in-place evolution slow to untangle, and made Enterprise Grid and shared channels expensive before any migration began. Convenience assumptions compound like debt.
- Active-active dual-writes bought availability at the price of the rest of the topology. Either side could serve reads and writes, so host failure was invisible to customers. But the configuration was nonstandard MySQL demanding custom tooling, and with no true replicas and direct application-to-host routing, replica reads were structurally unavailable. The design that kept Slack available in its early days became its operational ceiling as it grew.
- App-layer sharding was rejected not because it couldn't work but because it rebuilt the same wall one layer up. Prototypes showed the immediate scaling problem solvable, and the coupling untangling slow, the operational issues untouched, the replicas still unusable, and a future hot shard still hard to split. Slack's judgment call: when the routing layer is the problem, giving the routing layer more responsibilities is not the fix; changing which layer owns placement is.
- Staying on MySQL was the conservative constraint that made a radical migration survivable. Thousands of MySQL-specific queries and years of operational practice would have made NoSQL or NewSQL a rewrite of the company's muscle memory, not just its storage. Vitess let the risky thing (resharding everything) ride on the trusted thing (MySQL semantics underneath). The double-write-plus-double-read-diffing migration harness is the same philosophy applied to correctness: never cut over on faith.
- An application that no longer knows where its data lives buys that simplicity with a new dependency. The webapp no longer knows where data lives, which is the entire point. It also means the Vitess routing tier (query layer, lock server, topology metadata) is now load-bearing on every query's path, extended and operated by Slack until it became a top contributor to the project. Evicting placement knowledge from the application doesn't destroy the knowledge; it concentrates it in a tier that must be run superbly.
Patterns in this article
- Sharding Behind a Proxy
Slack's whole migration is this pattern: move shard placement out of the application and into a routing tier the datastore owns, Vitess's query layer and its lock-server layout. The application no longer knows where data lives, and the shard key can change from workspace id to channel id without product code knowing anything moved. The alternative Slack rejected marks the boundary: building sharding into the application was prototyped and declined, because the coupling ran too deep and a hot shard still would not split. It contrasts with Pinterest, which bakes placement into the application's own identifiers. Those are the two ends of one question: who is allowed to know where data lives, the application or the datastore.
- Single-Writer Ownership
In this story the pattern is restored by retiring its opposite. Slack's active-active pairs, with both sides taking writes, bought retry-through-failure availability at the cost of a nonstandard setup with no true replicas and no safe replica reads. The Vitess migration replaces those dual writers with ordinary single-primary replication per shard, and automated failovers restore the pattern's core benefit: one clear writer per slice of data, and a replica tier that can finally be trusted with reads.
- Universal Staged Rollout
The three-year migration is this pattern at datastore scale. A deliberately small first use case in production (RSS feeds into channels) forced every operational surface to exist early. Then tables were cut over one at a time under a generic backfill, with the application writing to both stores and a parallel system comparing reads to prove the two matched before any traffic committed. The result went from 0% to 99%, with the last percent scheduled, and a 50%-in-one-week COVID surge was absorbed mid-journey. Nothing cut over on faith, ever.
Also solving this
Other systems in behindscale's Single-cluster scaling ceiling class: