Slack, 2016–2026  / field guide
Practitioner field guide · 2026-10-16 · Distributed Systems

The workspace stopped being the shard: ten years of Slack

Slack was built in 2013 on one assumption: a workspace is a closed world, so it can live on one database shard and one message server. Over ten years, three product decisions broke that assumption. This guide rebuilds what Slack changed in response from its own postmortems and talks, from the Vitess commit history and from the branches of its Vitess fork. It shows where the old shard key kept causing outages after the migration was declared finished.

46 ledger rows 7 published incidents 1,950 Slack commits to Vitess, counted Evidence through October 2026 Read: 29 min
01

The territory

Why Slack, why now, and the one finding to carry out of this page.

Most writing about sharding treats the shard key as a database decision. Slack's record says it was a product decision, and so was every revision of it. In 2016, according to Mike Demmer's QCon talk, Slack gave each new workspace one database shard and one message-server shard, and almost everything a workspace did stayed on that shard. That was correct for a product where a channel, a user, an emoji and a file belonged to exactly one team. Then Slack shipped Enterprise Grid in 2017, which split one customer across many workspaces. Next came shared channels, later Slack Connect, which put one channel inside two companies. In 2023 and 2024 came Unified Grid, which made the organisation rather than the workspace the thing a user signs in to. Slack's own shared-channels post says the feature "challenged Slack's fundamental assumption that the workspace is the atomic unit of partitioning customer data".

Why this company, why now. Several earlier digs used Slack as a supporting example, for a scatter-read incident and for cells, but none covered it as a company. Its record is now unusually complete. Laura Nolan wrote three long postmortems. Two QCon SF 2024 talks cover the cellular migration and the Unified Grid re-architecture. Engineering posts as recent as October 2025 (Chef) and July 2026 (Shipyard) show the change path being partitioned the same way the request path was.

The surprise

The worst datastore outage Slack has documented came after the Vitess migration was reported as 99% complete, and the migration caused it. In February 2022 a group-DM membership lookup ran against a table sharded by user ID, so every cache miss became "a query on every shard". Database load rose "superlinearly in relation to the percentage of cache misses" (Slack, 2022). The git record shows Slack had treated the scatter as something to tolerate since 2018, when it upstreamed a directive that returns partial scatter results as warnings. A 2019 branch for a "limited scatter query planner" on Slack's fork never reached Vitess main. In this case the cache was what kept the database within capacity, and nobody had written down a capacity budget for it.

99%
of Slack query load served by Vitess, as of December 2020
5 min
to drain an availability zone after the cellular migration
3h14m
of the 2-22-22 outage (6:00 to 9:14 AM PST) from scatter reads on cache miss
786
Slack-authored Vitess commits in 2021, falling to 18 in 2022

Figure 1 · A decade of moving the partition

2016-2017Hack on HHVMEnterprise GridFlannelKafka before RedisVitess starts2018-2020Shared channelsPartial scatterHAProxy outageVitess 99%2021-2022TGW outageAZ gray failure2-22-22 scatterFork ends2023-2024AZ cellsUnified GridBedrock2025-2026Chef by AZFeb 2025 outageShipyardSlack 2016-2026
2016-2017Hack on HHVMEnterprise GridFlannelKafka before RedisVitess starts2018-2020Shared channelsPartial scatterHAProxy outageVitess 99%2021-2022TGW outageAZ gray failure2-22-22 scatterFork ends2023-2024AZ cellsUnified GridBedrock2025-2026Chef by AZFeb 2025 outageShipyardSlack 2016-2026
Product launches (Enterprise Grid, shared channels, Unified Grid) sit in the same columns as the platform changes they forced. The data partition moved three times, and the failure partition (AZ cells) and the change partition (Chef by AZ) were added separately. Compiled from the ledger; dates are publication or incident dates.
Diagram source

Scope. Covers the server-side architecture: partitioning, datastores, job queue, edge, real-time, deploy and host management, and the published incidents. Does not cover the desktop and mobile clients, search ranking, AI features, data warehouse or Salesforce integration, and Slack's costs, none of which the public record describes at architecture depth.

02

How it is actually built

The 2026 shape, reconstructed from Slack's own posts and talks. Each layer is attributed, and each still shows a trace of the workspace model.

Figure 2 · Slack's serving path, as published by 2026

Vitess keyspaces, cross-AZ

One AZ cell, siloed

xDS weights, 1% steps

miss

Clients

Edge: Envoy, gateway servers
multi-region

Webapp in Hack

Channel servers
consistent hash

Kafkagate

Kafka backlog

Redis job queue

memcached

by channel ID

by user ID

by org or workspace

Vitess keyspaces, cross-AZ

One AZ cell, siloed

xDS weights, 1% steps

miss

Clients

Edge: Envoy, gateway servers
multi-region

Webapp in Hack

Channel servers
consistent hash

Kafkagate

Kafka backlog

Redis job queue

memcached

by channel ID

by user ID

by org or workspace

Traffic is partitioned by availability zone and data by entity. The two partitions are independent, so a single request crosses one cell but may touch many shards. Sources: cells, 2023, real-time messaging, 2023, Vitess, 2020, job queue, 2017.
Diagram source

Webapp: PHP kept, typed incrementally

Slack moved from PHP 5 to HHVM in 2016 "mainly because it ran the existing PHP code faster". It then stayed on HHVM to adopt Hack's gradual types, so a large codebase could be typed one file at a time instead of being rewritten. The in-memory MySQL fake Slack published in 2019, hack-sql-fake, shows how deeply that codebase assumes MySQL semantics.

Sources: Adams 2016, Sandler 2020, hack-sql-fake

Datastore: MySQL behind Vitess, resharded by entity

The migration began in 2017. By December 2020, Vitess served 99% of queries at a 2.3M QPS peak, with a 2 ms median and 11 ms p99. Messages are sharded by channel ID. Some membership data is sharded by user ID. Org-level data has its own shards. Slack kept MySQL and replaced only the routing and topology layer.

Sources: Slack 2020, Percona Live 2018

Cache: memcached, quietly load-bearing

No Slack post describes the cache tier as a component in its own right. The 2-22-22 postmortem shows what it does: it absorbs reads that would otherwise scatter to every shard. On that reading, it is a capacity component with no published capacity budget.

Inferred from: Nolan 2022

Async work: Kafka in front of Redis

After Redis filled up and could not even dequeue, Slack put Kafka in front of it as a durable backlog. A Go service, Kafkagate, enqueues to Kafka. Redis was kept as the working queue. This was the minimum change that let Slack keep the job workers it already had. Throughput as of 2017 was 1.4B jobs a day, peaking at 33k per second.

Source: Slack 2017

Real-time: hashed channel servers

Channel servers hold channel state in memory and are placed by consistent hashing. Ring managers (CHARMs) replace an unhealthy host within 20 seconds, and a host carries around 16M channels at peak. Gateway servers hold each user's subscriptions and run in several regions. This tier was keyed by channel from the start, so the workspace model mattered least here.

Source: Slack 2023

Hosts and deploys: partitioned by AZ too

The webapp ships about 12 times a day: dogfood, then a 2% canary, then 10, 25, 50, 75 and 100%. Chef's single production environment was split into prod-1 to prod-6, mapped by availability zone. Shipyard (2026) turns EC2 hosts into immutable artifacts with metric-based rollback. Most applications run on Bedrock (EKS plus Karpenter).

Sources: deploys, Chef 2025, Shipyard 2026

Figure 3 · The unit of partition, three times over

Enterprise Grid,
shared channels

Unified Grid

local read

membership by user

per request

2013-2017
workspace = shard
token names workspace

2017-2020
entity keys in Vitess
channel, user

2023-2024
token names org
check org + member workspaces

1 shard

every shard on miss

org shard + N workspaces

Enterprise Grid,
shared channels

Unified Grid

local read

membership by user

per request

2013-2017
workspace = shard
token names workspace

2017-2020
entity keys in Vitess
channel, user

2023-2024
token names org
check org + member workspaces

1 shard

every shard on miss

org shard + N workspaces

Every move put an object that used to live in one place into several places. Each move also turned a class of local reads into fan-out reads, which a cache or a second copy then had to absorb. Sources: Demmer 2018, shared channels, Hoffman 2024.
Diagram source

Each layer adopted a different key, and the order matters. The real-time tier used channel keys from the start, so shared channels cost it little. The datastore moved to entity keys between 2017 and 2020. Request routing still read a workspace ID from the session token until Unified Grid. In the third-party write-up of Ian Hoffman's QCon talk, the token after Unified Grid "no longer determines the workspace shard". It names the org, and the server checks the org shard and then the shard of each workspace the user belongs to. On that description, Slack's highest-volume request path now does a bounded fan-out by design. It was the right trade for the product. It also means per-request shard count grows with how many workspaces a user is in, and Slack has not published that distribution.

03

The decisions that matter

What Slack chose, what it passed over and why, and the condition under which the choice would go the other way.

Decision: what is the shard key when tenants start sharing objects?

Chosen
  • Re-key each table by the entity its hottest query reads: messages by channel, some membership by user, org data by org (2017 to 2020, Vitess).
  • The tenant key had no room left: a big customer filled one host and could not grow (CNCF case study). Shared channels also made "which tenant owns this row" unanswerable for some rows.
Passed over
  • Keep workspace sharding and copy shared objects into each workspace's shard.
  • Slack's excerpts do not give the reason. The likely reason (inferred) is that a copy per workspace multiplies writes and consistency work for every shared channel.
Flips when
  • Objects almost never cross tenants and tenants fit on a host. In that case the tenant key keeps every read local, which is what Slack had until 2017.

Decision: what does a query do when it must touch every shard?

Chosen (2018)
  • Tolerate it. Slack upstreamed SCATTER_ERRORS_AS_WARNINGS (PRs #4228 and #4293), so a scatter returns partial results when some shards fail.
  • This keeps reads available while shards are degraded.
Not taken
  • Limit scatter in the planner: a 2019 branch on Slack's fork, never merged upstream.
  • Refuse scatter outright: PlanetScale added no_scatter in 2021 (#8439). Nothing published says whether Slack enables it.
Flips when
  • A cache sits in front of the scatter. A miss storm then multiplies the load instead of degrading it, and the right answer becomes a second copy of the data keyed for that query. Slack's own postmortem names the channel-keyed membership keyspace as the efficient one.

Decision: how do you contain a failure in one availability zone?

Chosen
  • Silo each AZ as a cell and route into it with Envoy/xDS weights, drained in 1% steps within five minutes (2021 to 2023).
  • The request path is written in Hack, Go, Java and C++, so routing logic placed at the edge was cheaper than repeating it in each service.
Passed over
  • Teach each service to detect and avoid a bad AZ.
  • The June 2021 gray failure showed why: hosts inside the zone saw full connectivity while hosts outside saw it as down. Per-service detection disagreed with itself.
Flips when
  • Your stack is one runtime with a shared client library. A library-level fix is then one change rather than four, which is the position Netflix was in (see the earlier Netflix dig).

Figure 4 · When your tenant stops being your shard

no

yes

no

yes

no

yes

Can one object
belong to 2+ tenants?

Keep tenant key
(Slack to 2017)

Does a hot query read
by the other noun?

Re-key by entity
(Vitess, 2017-20)

Is a cache absorbing
the fan-out?

Bounded scatter,
partial results OK (2018)

Second copy keyed
for that query, or a
miss-rate budget (post-2022)

no

yes

no

yes

no

yes

Can one object
belong to 2+ tenants?

Keep tenant key
(Slack to 2017)

Does a hot query read
by the other noun?

Re-key by entity
(Vitess, 2017-20)

Is a cache absorbing
the fan-out?

Bounded scatter,
partial results OK (2018)

Second copy keyed
for that query, or a
miss-rate budget (post-2022)

A decision tree derived from Slack's sequence of choices. Each terminal is an action Slack took, with the year it took it. The tree itself is this guide's reconstruction; Slack published no such rule.
Diagram source
DecisionChosenPassed overBecauseFlips whenEvidence
Language of the webappPHP to HHVM to Hack, typed incrementally (2016)Rewrite in another languageSpeed first, then types without a rewriteThe runtime's community collapses; Slack still depends on HHVM in 2026Sandler 2020
Job queue durabilityKafka in front of Redis (2017)Replace Redis with KafkaSmallest change that removes the memory ceiling and leaves workers aloneThe worker fleet is being rewritten anywaySlack 2017
Client bootEdge cache that lazy-loads (Flannel, 2017)Full workspace model at loginLarge teams made the full model unworkableTeams stay small; the full model is simplerWei 2017
Vitess: fork or upstreamFork with date-stamped sync branches (2017 to 2021), then upstream releasesStay on a long-lived forkInferred: the fork's cost grew with every upstream releaseUpstream rejects a change you cannot live withouttinyspeck/vitess branches
Websocket edgeEnvoy (2021)Dedicated HAProxy fleetsDynamic config without reloads; the 2020 outage was a reload workaround that failedBackend sets change rarelySlack 2021
Request routing keyOrg in the token, then fan out to member workspaces (Unified Grid)Workspace in the tokenUsers belong to many workspaces; switching and duplicated client data cost moreMost users belong to one workspaceSlack 2024
Config management rolloutChef split into six AZ-mapped environments, run on signalOne production environment on cronA bad cookbook reached every AZ at onceThe fleet is one AZSlack 2025

No architecture decision records were published. Slack has not released ADRs or RFCs for any of these choices. Its reasoning appears only in blog posts and conference talks, which give the option chosen far more often than the options turned down. The "passed over" column above is the guide's reconstruction wherever a cell says "inferred". This is the main limit of the record: you can see what Slack did, but rarely what it argued against.

04

What broke in production

Seven published incidents, grouped by failure class rather than by date. Three of them come from the partition key, and three from glue that sat in the request path without anyone treating it as part of it.

Class A · The partition key amplifies a miss

Postmortem

2-22-22: a cache restart turned membership reads into scatters

AssumptionThe cache hit rate is high enough that the user-sharded membership table never sees a channel-keyed read.
What happenedAccording to a secondary reading of the postmortem, a Consul agent upgrade restarted memcached nodes and the hit rate fell. Each miss for a group DM's members queried every shard. Load grew superlinearly with the miss rate, and client retries kept it high.
Blast radius6:00 to 9:14 AM PST, users unable to load Slack. The first increase to the boot-throttle limit was too large and pushed load back up.
FixThrottle client boots, raise the limit in small steps, serve this immutable data from replicas as well as primaries.
Design ruleIf a cache sits in front of a scatter read, the miss rate is a capacity parameter. Budget it, alert on it, and remove the scatter with a second keyspace if the budget is thin.
Postmortem

The Query Strikes Again: one tenant's purge fills the shared datastore

AssumptionBackground jobs are throttled well enough that one customer's bulk action cannot starve interactive reads.
What happenedAn async job purging a large volume of records overloaded the datastore in October 2022. Shimming the job stopped it on day one. On day two it came back worse, and an edge-case bug in datastore automation could not handle the resulting flood of requests.
Blast radiusSome customers could not load Slack. Features were disabled until the job finished.
FixPer the post: job-level kill switch (the shim), then load shedding by feature.
Design ruleOnce tenant and shard no longer coincide, per-tenant isolation has to be enforced in the job system, because the database can no longer provide it.
Status page

February 2025: maintenance meets a latent cache defect

AssumptionDatabase maintenance is safe while the cache works normally.
What happenedA maintenance action in the database systems and a latency defect in the caching system together overloaded a database. About half of the instances relying on it became unavailable.
Blast radius6:45 AM to 4:13 PM PST on 26 February. Events API and Slack Connect mention effects resolved the next day.
FixLoad reduced, and a fix deployed for the source of the extra traffic. No engineering postmortem has been published.
Design ruleThe same pairing as 2022, cache behaviour plus datastore change. Treat cache hit rate as a precondition checked before any datastore maintenance begins.

Figure 5 · The 2-22-22 failure path

Vitess, membershipby userWebappmemcachedConsul agentupgradeVitess, membershipby userWebappmemcachedConsul agentupgradeload superlinear inmiss raterestart nodes, hit rate fallswho is in this group DM?missquery every shardslow, some time outclient retries add loadthrottle client bootsread replicas too, raise limit insteps
Vitess, membershipby userWebappmemcachedConsul agentupgradeVitess, membershipby userWebappmemcachedConsul agentupgradeload superlinear inmiss raterestart nodes, hit rate fallswho is in this group DM?missquery every shardslow, some time outclient retries add loadthrottle client bootsread replicas too, raise limit insteps
The trigger was an infrastructure upgrade, the amplifier was the shard key, and retries kept the overload going. Recovery depended on throttling client boots, because the database could not be scaled fast enough. Reconstructed from Nolan 2022; the Consul step comes from a secondary reading.
Diagram source

Class B · Glue in the request path

Postmortem

May 2020: HAProxy ran out of server slots

AssumptionThe program that writes healthy backends into HAProxy's pre-allocated slots always has a free slot.
What happenedSlack avoided reloads by using HAProxy's runtime API and fixed server-template slots, filled from Consul via consul-template. The sync program claimed slots for new instances before freeing dead ones. When it found none free it exited early, and HAProxy state went stale for hours. Evening scale-down then terminated the older instances HAProxy still pointed to.
Blast radiusCustomer-visible 4:45 to 5:33 PM PDT. The fault began around 8:30 AM. Adding 75% more instances did not help.
FixA rolling HAProxy restart. Later, the websocket edge moved to Envoy with dynamic configuration (2021).
Design ruleA workaround for reload cost is now part of the control plane. Give it the alerting and tests a control plane gets. Slack's alert for this exact case existed but was not working.
Postmortem

January 2021: the fix depended on the broken network

AssumptionWhen the web tier saturates, autoscaling adds capacity.
What happenedReturning users with stale caches made heavy boot requests. AWS Transit Gateways "did not scale fast enough". Slack tried to add 1,200 web servers, but provision-service needed the same degraded network and hit the Linux open-files limit and an AWS quota. Dashboards were lost because monitoring sat in its own VPC behind the gateways.
Blast radiusThe first working day of 2021. Elevated errors until 10:40 AM PST.
FixAWS added TGW capacity manually and reviewed its scaling algorithms. Slack raised the limits on provisioning.
Design ruleCapacity-adding machinery and monitoring must not share a dependency with the thing that saturates. Test scale-up during a network brown-out, not on a quiet day.
Postmortem

2017: the job queue filled up and could not drain

AssumptionAn in-memory queue can absorb a backlog while the database recovers.
What happenedDatabase contention slowed job execution and Redis reached its memory limit. Dequeueing also needs free memory, so the queue stayed stuck after the database recovered.
Blast radiusEvery async path: message posts, push notifications, unfurls, reminders, billing.
FixKafka as a durable backlog in front of Redis, written to by Kafkagate.
Design ruleA buffer must be able to drain from a full state. Put the overflow in a store whose capacity does not compete with the drain.

Class C · Gray failure across zones

Incident in design post

June 2021: one AZ looked healthy from inside and dead from outside

AssumptionAn AZ is either up or down, and health checks can tell which.
What happenedA network link serving one US-East AZ failed intermittently. It was removed, passed automated checks, returned, and failed again the same day. Systems inside the zone saw full connectivity while systems outside saw it as unavailable.
Blast radiusDegraded service for Slack customers. The excerpts do not give a precise duration.
FixAZ-siloed cells, drained at the edge in 1% steps, built over 1.5 years. Later, Chef rollouts partitioned along the same AZ lines.
Design ruleDo not rely on in-band detection to evacuate. Give operators an out-of-band drain that depends on nothing inside the zone being drained.

What the seven incidents have in common

None of them was a database losing data or a consensus bug. All three Class A incidents happened where a read keyed by one noun hit a table keyed by another, with a cache holding the difference. All three Class B incidents happened in machinery that was supposed to add or route capacity, and that machinery failed under the same conditions that made capacity necessary. The Huang et al. study of 22 metastable failures across 11 organisations (OSDI '22) found retries to be the most common sustaining effect. 2-22-22 fits that pattern: once the trigger passed, the overload continued on its own.

Corroborated across: OSDI '22, Hochstein 2021

05

Numbers you can plan against

Every figure with its date. Rows marked "secondary" come from a summary, not Slack's own page.

MetricValueAtContextAs ofSource
Datastore peak2.3M QPSSlackVitess, median 2 ms, p99 11 ms2020-12Slack
Share of query load on Vitess99%SlackMigration started 20172020-12Slack
Async jobs1.4B / daySlackPeak 33,000 per second2017-12Slack
Edge cache connections4MSlackFlannel, 600k client QPS (secondary)2017StackShare
Channels per channel server~16MSlackPeak; CHARM replaces a host in <20 s2023Slack
Webapp deploys~12 / daySlack2% canary then 10/25/50/75/1002020Slack
AZ drain time5 minSlack1% granularity, 99.99% SLA (secondary)2023InfoQ
Apps on Bedrock (EKS)>80%SlackSecondary2024-05InfoQ
Emergency scale-up attempted1,200 hostsSlackJan 2021, mostly never served2021-01The Register
Instances unavailable, Feb 2025~50%SlackOf those relying on the affected DB2025-02Slack status
Slack-domain commits to Vitess1,950vitessio/vitessPeak 786 in 2021, 18 in 2022, 13 in 2026 to Oct 122026-10git log, measured
Slack fork branches88tinyspeck/vitessslack-* branches; last versioned tip 2022-03-292026-10git ls-remote, measured
Read these carefully

Measured by this guide: the two git rows. They count commit author addresses ending in slack-corp.com, so they miss Slack engineers who committed under personal addresses (Demmer also appears as demmer@gmail.com), and they undercount. Reported by Slack: the datastore, job, real-time and deploy rows. Secondary: Flannel, drain time and Bedrock share. Unknown, and the figures an architect most needs: the cache hit rate the membership path needs to stay within capacity, the per-request shard fan-out after Unified Grid, and any cost figure. Slack has published none of these. Every Slack figure here is at least a year old, apart from the Chef and Shipyard design descriptions.

A derived figure, labelled as such. Suppose a membership read normally hits cache and a miss fans out to every shard. Database reads per request are then roughly m × S, where m is the miss rate and S the shard count. A keyspace sharded by channel would cost m × 1. On 100 shards, a drop in hit rate from 99% to 90% multiplies membership load on the database tenfold, from 1 to 10 shard-reads per request, against 0.01 to 0.1 for the channel-keyed copy. The arithmetic is linear in m. Slack observed superlinear growth, which only happens if per-query latency also rises with load and retries add queries. That is the metastable loop, and it is this guide's inference, not a figure Slack published.

06

What ten years of this teaches

Three claims, each tied to evidence, and the rule they add up to.

1. The shard key encodes the product's ownership model, and only product decisions change it. All three moves of Slack's partition (workspace, then entity, then org) followed a product launch. None of them followed a database limit on its own. The CNCF case study shows that the workspace key had already run out of room for the largest customers. Slack's own shared-channels post shows the key stopped matching the data once channels crossed companies. When product managers plan a feature that lets an object belong to two of anything, the database team should be in the room.

2. A migration is not done when the queries move. It is done when no read crosses the old key. The Vitess post reported 99% of queries migrated in December 2020. Fourteen months later, the worst datastore incident in the record came from a read that still asked by channel against data now keyed by user. Slack's 2018 scatter directive and its unmerged 2019 limited-scatter branch show the cost was known and treated as an availability problem, by returning partial results, rather than a capacity problem. The postmortem's own remedy points to a second copy keyed by channel. When you re-key a table, list every query that reads it by a different noun. Each one needs a second keyspace, a capacity budget for the cache in front of it, or a test that runs it with the cache cold.

3. Slack ended up with three partitions, and each answers a different question. Data is partitioned by entity, which decides where a row lives. Traffic is partitioned by availability zone, which decides where a failure can spread. Change is partitioned by availability zone as well: Chef prod-1 to prod-6, progressive deploys, Shipyard. That one decides how far a bad change can reach. Slack did not plan this. Each partition was added after an incident: the job queue in 2017, the AZ gray failure in 2021, and the cookbook blast radius described in the 2025 Chef post. Cells did not remove Class A risk, because Vitess keyspaces still span zones. A cell drain cannot fix a scatter storm.

The architectural lesson

Choose your partition keys for the ownership model the product will have, not the one it has today. Keep the data, failure and change partitions as separate design decisions. Then, for every read that crosses a partition, write down what absorbs the fan-out and what that component's capacity is. At Slack the unwritten answer was "the cache". Its budget was discovered on 22 February 2022.

07

The evidence wall

Every source behind this page, graded. Non-git sources were read through search-engine excerpts because direct fetches were blocked in this session; see the ledger.

PostmortemSlack2022-04

Slack's Incident on 2-22-22

A membership table sharded by user turned each cache miss into a query on every shard, and load grew superlinearly with the miss rate. The postmortem names a channel-keyed keyspace as the efficient alternative.

Carry forwardA cache in front of a scatter read is a capacity component.
slack.engineering/slacks-incident-on-2-22-22
PostmortemSlack2021-02

Slack's Outage on January 4th 2021

Transit Gateways saturated. Provisioning, autoscaling and monitoring all depended on the same network.

Carry forwardThe recovery path must not share the failing dependency.
slack.engineering/slacks-outage-on-january-4th-2021
PostmortemSlack2020-07

A Terrible, Horrible, No-Good, Very Bad Day at Slack

HAProxy server-slot sync exited early when no slot was free. State went stale, and scale-down then removed live backends.

Carry forwardReload workarounds are control planes; test them like one.
slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack
PostmortemSlack2022-10

The Query Strikes Again

A long-running purge job overloaded the datastore on two consecutive days. The job was shimmed to stop it.

Carry forwardTenant isolation moves into the job system once tenant is not shard.
slack.engineering/the-query-strikes-again
PostmortemSlack status2025-02

Wednesday February 26, 2025

Database maintenance and a caching latency defect together left about half of dependent instances unavailable for most of a day.

Carry forwardCheck cache health before datastore maintenance.
slack-status.com/2025-02-26
PostmortemSlack status2022-02

Tuesday February 22, 2022

The customer-facing window, 6:00 to 9:14 AM PST, and a trigger described as a configuration change.

Carry forwardThe status page dates the outage; the postmortem gives the mechanism.
slack-status.com/2022-02-22
SourceVitess2017–2026

vitessio/vitess commit history

1,950 commits from slack-corp.com addresses, peaking at 786 in 2021 (VTAdmin, workflow RPCs) and falling to 18 in 2022.

Carry forwardCommit authorship dates when a company stopped carrying a dependency.
github.com/vitessio/vitess
SourceSlack via Vitess2018-10

PR #4228 partial-scatter (and #4293)

Slack's directive, renamed from SHARD_PARTIAL to SCATTER_ERRORS_AS_WARNINGS, returns partial results when some shards fail.

Carry forwardScatter was treated as an availability problem first.
github.com/vitessio/vitess/pull/4228
SourcePlanetScale via Vitess2021-07

PR #8439 no-scatter

An operator flag that fails any plan containing a scatter. It was added upstream three years after Slack's partial-scatter directive.

Carry forwardMaking scatter impossible is a deployable option, not only a code-review rule.
github.com/vitessio/vitess/pull/8439
SourceSlack2019-04

tinyspeck/vitess: brirams_limited_scatter_query_planner

A 2019 Slack branch for limiting scatter in the planner. A merge-base check shows it never reached Vitess main. The guide found no PR record for it.

Carry forwardThe rejected-or-abandoned fix is visible only in the fork.
github.com/tinyspeck/vitess/tree/brirams_limited_scatter_query_planner
SourceSlack2017–2022

tinyspeck/vitess branch inventory

88 slack-* branches. Date-stamped sync and release branches run from 2017 to 2020, then version-stamped ones (8, 9, v10, v11) in 2021, ending 2022-03-29.

Carry forwardBranch naming dates the change from running a fork to running releases.
github.com/tinyspeck/vitess
SourceSlack2019-05

slackhq/hack-sql-fake

An in-memory MySQL simulation for Hack unit tests, still maintained in 2026.

Carry forwardTest tooling shows which engine semantics a codebase depends on.
github.com/slackhq/hack-sql-fake
SourceSlack2020–2026

slackhq/astra

A log and trace search engine Slack built itself, started as KalDB in 2020 and renamed in April 2024. Its goals include "coordination free ingestion".

Carry forwardSlack builds in-house where a vendor's cost or coordination model does not fit.
github.com/slackhq/astra
Case studySlack2020-12

Scaling Datastores at Slack with Vitess

Migration from 2017, 99% of query load, 2.3M QPS peak, 2 ms median and 11 ms p99. Messages re-keyed by channel.

Carry forward"99% migrated" measured queries, not the cross-key reads that remained.
slack.engineering/scaling-datastores-at-slack-with-vitess
Eng blogSlackn.d.

How Slack Built Shared Channels

States outright that shared channels broke the assumption that the workspace is the atomic unit of partitioning.

Carry forwardShared objects are a partitioning change.
slack.engineering/how-slack-built-shared-channels
Eng blogSlack2024

Unified Grid

Slack was re-oriented around the user and the organisation instead of the workspace, after Enterprise Grid made users members of many workspaces.

Carry forwardRouting keys change last and cost the most.
slack.engineering/unified-grid
Eng blogSlack2017-12

Scaling Slack's Job Queue

Redis filled up and could not dequeue. Kafka was put in front as a durable backlog. 1.4B jobs a day.

Carry forwardThe smallest change that removes the ceiling.
slack.engineering/scaling-slacks-job-queue
Eng blogSlack2023-04

Real-time Messaging

Channel servers placed by consistent hash, CHARM replacement in under 20 seconds, around 16M channels per host.

Carry forwardThe tier keyed by channel from the start was the cheapest to change.
slack.engineering/real-time-messaging
Eng blogSlack2017-05

Flannel: An Application-Level Edge Cache

Large teams made it unworkable to download the full model at boot, so data is loaded lazily at the edge.

Carry forwardThe workspace model failed on the client before the database.
slack.engineering/flannel
Eng blogSlack2020

Deploys at Slack

About 12 deploys a day, with dogfood, a 2% canary and percentage steps.

Carry forwardProgressive delivery limited code changes long before host changes.
slack.engineering/deploys-at-slack
Eng blogSlack2026-07

Shipyard: Slack's Next-Generation EC2 Platform

Hosts become immutable layered AMIs with metric-based rollback. Data nodes are named as future work.

Carry forwardStateful nodes are the last to become immutable.
slack.engineering/shipyard
AnalysisLorin Hochstein2021-02

Slack's Jan 2021 outage: a tale of saturation

An independent reading of the January 2021 postmortem as saturation spreading across layers.

Carry forwardSaturation spreads through dependencies a component diagram does not show.
surfingcomplexity.blog
PaperHuang et al., HotOS2017

Gray Failure: The Achilles' Heel of Cloud-Scale Systems

Differential observability: failure detectors see health while applications see failure. Slack's June 2021 incident fits this exactly.

Carry forwardEvacuate on application signals, not health checks.
microsoft.com, HotOS '17 paper
PaperHuang et al., OSDI2022

Metastable Failures in the Wild

22 failures across 11 organisations, with retries the most common sustaining effect. This matches how 2-22-22 behaved.

Carry forwardRecovery needs load shedding at admission, which Slack did by throttling client boots.
usenix.org/conference/osdi22
TalkMike Demmer, QCon SF2018-11

Scaling Slack: The Good, the Unexpected, and the Road Ahead

The workspace-sharded starting point, and the three changes: decomposing the message server, edge caching, and Vitess.

Carry forwardThe baseline every later change departs from.
infoq.com/presentations/slack-scalability-2018
TalkIan Hoffman, QCon SF2024-11

Changing the Model: Why and How We Re-Architected Slack

The token names the org, and the server checks the org shard plus member workspace shards. A daily-use prototype came first.

Carry forwardPrototype a model change on employees before migrating customers.
infoq.com/presentations/slack-rearchitecture
TalkCooper Bethea, QCon SF2024-11

Slack's Migration to a Cellular Architecture

The talk version of the cells migration, from the engineer who led it.

Carry forwardCells were retrofitted service by service over 1.5 years.
infoq.com/presentations/slack-cellular-architecture
VendorCNCFn.d.

Slack case study

Workspace sharding put all of a customer's load on one cluster, with nowhere to go when it outgrew a host.

Carry forwardUse it to name the constraint, not for outcome claims.
cncf.io/case-studies/slack
08

Build a miniature, then productionise it

Reproduce Slack's partition migration and its failure mode at a scale you can break on a laptop.

Shard by tenant

Four MySQL or Postgres instances. Route each workspace to one by hash of workspace ID, and store channels and memberships there.

Done when: every API call touches exactly one database.  Teaches: why the 2013 design was right for its product.

Add a shared channel

Let one channel belong to two workspaces on different shards. Try putting it on one side, then copying it to both.

Done when: you can name the read that becomes cross-shard under each option.  Teaches: the assumption shared channels broke.

Re-key with a router

Put Vitess (or a thin router) in front. Shard messages by channel and membership by user, and move the data with dual writes and read diffing.

Done when: the router reports zero tenant-keyed queries.  Teaches: "migrated" counts queries, not access patterns.

Put a cache in front, then restart it

Cache group-membership lookups. Under load, restart half the cache nodes and watch per-shard QPS. Add client retries with backoff.

Done when: you have a curve of database load against miss rate, including the knee.  Teaches: the 2-22-22 mechanism and the miss-rate budget.

Fix it two ways

First add admission control on client boot. Then add a membership copy sharded by channel, kept in sync from the primary write, and rerun the cache restart.

Done when: the cold-cache curve stays flat with the second keyspace.  Teaches: shedding load buys time; a second key removes the problem.

Silo by zone and drain

Run two copies of the stateless tier in two "zones" behind Envoy with weighted clusters. Inject packet loss only between zones, then drain one in 1% steps.

Done when: a drain completes with no client-visible errors and depends on nothing in the drained zone.  Teaches: gray failure, and why the drain must be out-of-band.

Partition the change path

Make every config or image change roll out one zone at a time, triggered by a signal, with automatic rollback on an error-rate metric.

Done when: a deliberately bad config never reaches the second zone.  Teaches: the third partition, the one Slack added last.

09

Keep hunting

The queries and commands that found the material above.

Slack's own record

  • slack.engineering incident "sharded by user ID" cache
  • slack.engineering "workspace is the atomic unit"
  • slack-status.com 2025-02-26
  • qconsf.com Slack 2024 re-architected cellular

Git archaeology

  • git log --format='%ad|%ae' --date=format:%Y | grep slack-corp | cut -d'|' -f1 | sort | uniq -c
  • git ls-remote https://github.com/tinyspeck/vitess | grep refs/heads/slack
  • git merge-base --is-ancestor ts/brirams_limited_scatter_query_planner HEAD
  • git log --oneline | grep -iE 'scatter.*(warning|limit)|no_scatter'
10

References

  1. Laura Nolan, Slack's Incident on 2-22-22 Slack Engineering, 2022-04-26. Checked 2026-10-16.
  2. Slack status, February 22, 2022 Slack, 2022. Checked 2026-10-16.
  3. yfractal, Slack's Incident on 2-22-22 Reading Note Medium, 2022. Checked 2026-10-16.
  4. Laura Nolan, Slack's Outage on January 4th 2021 Slack Engineering, 2021-02. Checked 2026-10-16.
  5. Slack fingers AWS auto-scaling failure in January outage postmortem The Register, 2021-02-02. Checked 2026-10-16.
  6. Lorin Hochstein, Slack's Jan 2021 outage: a tale of saturation 2021-02-08. Checked 2026-10-16.
  7. Laura Nolan, A Terrible, Horrible, No-Good, Very Bad Day at Slack Slack Engineering, 2020. Checked 2026-10-16.
  8. Slack status, May 12, 2020 Slack, 2020. Checked 2026-10-16.
  9. The Query Strikes Again Slack Engineering, incident 2022-10. Checked 2026-10-16.
  10. Slack status, February 26, 2025 Slack, 2025. Checked 2026-10-16.
  11. Scaling Datastores at Slack with Vitess Slack Engineering, 2020-12. Checked 2026-10-16.
  12. Simon Willison, link note on the Vitess post 2020-12-01. Checked 2026-10-16.
  13. Percona Live 2018: Migrating to Vitess at (Slack) Scale Percona, 2018. Checked 2026-10-16.
  14. CNCF, Slack case study CNCF, n.d. Checked 2026-10-16.
  15. How Slack Built Shared Channels Slack Engineering, n.d. Checked 2026-10-16.
  16. Unified Grid: How We Re-Architected Slack for Our Largest Customers Slack Engineering, 2024. Checked 2026-10-16.
  17. Ian Hoffman, Changing the Model (QCon SF 2024) InfoQ, 2025-04-18. Checked 2026-10-16.
  18. Mike Demmer, Scaling Slack (QCon SF 2018) InfoQ, 2018. Checked 2026-10-16.
  19. Scaling Slack, QCon SF 2018 slides QCon, 2018. Checked 2026-10-16.
  20. Slack's Migration to a Cellular Architecture Slack Engineering, 2023-08. Checked 2026-10-16.
  21. Slack Migrates to Cell-Based Architecture on AWS InfoQ, 2024-01. Checked 2026-10-16.
  22. Cooper Bethea, Slack's Migration to a Cellular Architecture (QCon SF 2024) InfoQ. Checked 2026-10-16.
  23. Huang et al., Gray Failure: The Achilles' Heel of Cloud-Scale Systems HotOS '17. Checked 2026-10-16.
  24. Huang et al., Metastable Failures in the Wild OSDI '22. Checked 2026-10-16.
  25. Scaling Slack's Job Queue Slack Engineering, 2017-12. Checked 2026-10-16.
  26. Bing Wei, Flannel Slack Engineering, 2017-05-31. Checked 2026-10-16.
  27. StackShare, Flannel summary 2017. Checked 2026-10-16.
  28. Keith Adams, Taking PHP Seriously Slack Engineering, 2016-10-13. Checked 2026-10-16.
  29. Scott Sandler, Hacklang at Slack: A Better PHP Slack Engineering, 2020-04. Checked 2026-10-16.
  30. Deploys at Slack Slack Engineering, 2020. Checked 2026-10-16.
  31. Migrating Millions of Concurrent Websockets to Envoy Slack Engineering, 2021. Checked 2026-10-16.
  32. Real-time Messaging Slack Engineering, 2023. Checked 2026-10-16.
  33. Applying Product Thinking to Slack's Internal Compute Platform Slack Engineering, 2024. Checked 2026-10-16.
  34. InfoQ, Slack on EKS and Karpenter InfoQ, 2024-05. Checked 2026-10-16.
  35. Advancing Our Chef Infrastructure: Safety Without Disruption Slack Engineering, 2025-10-23. Checked 2026-10-16.
  36. Shipyard: How We Built Slack's Next-Generation EC2 Platform Slack Engineering, 2026-07. Checked 2026-10-16.
  37. vitessio/vitess, git history GitHub, read 2026-10-16.
  38. vitessio/vitess PR #4228, partial-scatter GitHub, merged 2018-10-27. Checked 2026-10-16.
  39. vitessio/vitess PR #4293, partial-scatter-with-warnings GitHub, merged 2018-10-29. Checked 2026-10-16.
  40. vitessio/vitess PR #8439, no-scatter GitHub, merged 2021-07-13. Checked 2026-10-16.
  41. tinyspeck/vitess, Slack's fork GitHub, read 2026-10-16.
  42. slackhq/hack-sql-fake GitHub, read 2026-10-16.
  43. slackhq/astra GitHub, read 2026-10-16.