Post-mortem: GitHub MySQL replication lag
A post-mortem of GitHub's extended MySQL replication lag incident, where a schema migration on a large table caused persistent replica lag affecting read traffic for hours.
Takeaway
Large-table schema changes can turn ordinary asynchronous replication lag into a platform incident. A blocking DDL event queues writes on replicas, stale reads trigger fallback traffic, and that fallback can overload the primary. The durable response is to canary migrations, monitor lag and recovery rate, keep a kill switch, and size the system for bounded staleness and fallback load.
Scope and Evidence
Composite analysis: facts versus illustrative reasoning
This page explicitly combines multiple GitHub-documented replication and migration themes into one representative scenario; it does not claim that the exact timeline below occurred as one incident. Public materials support the general technologies and trade-offs. The single migration, timestamps, table size, write rate, replica topology, user symptoms, and calculations are illustrative unless a passage says otherwise. Treat them as a model for reasoning, not as unpublished GitHub telemetry.
5-Minute Incident Walkthrough
- Trigger: A large schema migration emits work that replicas cannot apply at the primary's write rate.
- Queue: The replica SQL applier blocks on DDL while subsequent binlog events accumulate.
- User impact: Lagged replicas return stale or missing data, which looks like an application inconsistency.
- Amplification: Lag-aware routing sends more reads to the primary, increasing CPU, connections, and write latency.
- Recovery: Pause or throttle the migration, preserve a healthy read path, and let replicas drain faster than new events arrive.
Causal chain: large-table DDL β serial replica apply β binlog backlog β stale reads β primary fallback load β slower writes and binlog production β longer recovery.
Incident Summary
Date: Composite incident (GitHub has documented multiple replication lag events; this is a representative analysis based on public disclosures) Duration: Representative scenario: 4-8 hours of elevated replica lag, with partial degradation continuing for 12+ hours Systems affected: In the modeled topology, MySQL read replicas serving GitHub.com features such as issue trackers, pull request views, repository listings, and user profile data Impact: In the representative scenario, read queries return stale data. Users see missing issues, outdated pull request statuses, and phantom "file not found" errors on recently created repos. Some reads fall back to primary, increasing primary load to dangerous levels. Root cause: In the modeled scenario, a schema migration (ALTER TABLE or gh-ost operation) on a multi-billion-row table causes replica lag to grow from milliseconds to hours. Single-threaded replication on replicas means large DDL operations block subsequent replication events, creating a cascading stale-data problem across the platform.
GitHub runs one of the world's largest MySQL deployments. At this scale, operations that are routine for smaller databases become risky production events. Schema migrations sit at the intersection of every database risk factor: table locking, replication lag, disk I/O, and query plan invalidation. This composite scenario illustrates what happens when those factors converge on a table with billions of rows and thousands of writes per second.
The scenario is a useful counterexample to the idea that "it's just an ALTER TABLE, we'll run it during low traffic." Schema migrations on large tables at GitHub's scale are production deployments that deserve the same caution as a code release.
What Happened: The Timeline
The timeline below is a representative reconstruction for the composite analysis. Its timestamps, row counts, lag values, and user symptoms are illustrative unless tied to a cited public disclosure.
| Time | Event |
|---|---|
| 10:00 AM | DBA team initiates schema migration on a 2+ billion row table via gh-ost |
| 10:15 AM | Replicas begin applying the DDL changes from the binary log |
| 10:30 AM | Replica lag reaches 5 minutes, monitoring alerts fire |
| 11:00 AM | Lag crosses 15 minutes; stale data reports begin from users |
| 11:30 AM | Lag reaches 45 minutes; users report missing issues and outdated PR statuses |
| 12:00 PM | ProxySQL lag threshold exceeded; read traffic falls back to primary |
| 12:30 PM | Primary CPU and connection count spike from redirected reads |
| 1:00 PM | Primary approaches capacity limits; write latency increases |
| 2:00 PM | DBA team pauses the migration; replicas begin draining the backlog |
| 2:00-6:00 PM | Replicas slowly catch up on queued binlog events |
| ~6:00 PM | Replica lag recovers to sub-second levels; normal read routing resumes |
The timeline reveals the fundamental problem with large DDL operations on replicated databases: the impact is not immediate, it is cumulative. Lag starts small and grows linearly. By the time it is noticeable to users, the backlog is already large enough that recovery takes hours even after the migration is paused.
The user-visible symptoms were varied and confusing. A developer opens a pull request at 11:00 AM. They see it in their dashboard (served from the primary via write-read-your-own-writes routing). Their teammate refreshes the PR list at 11:05 AM but the PR does not appear (served from a replica that is 40 minutes behind). The teammate thinks the developer has not opened the PR yet. Meanwhile, CI status checks that ran against a lagged replica returned outdated file contents for the diff. The symptoms looked like "GitHub is randomly broken" rather than "GitHub has replication lag," which made it harder for users and support to pinpoint the issue.
Another symptom: merge conflicts that should not exist. If a developer pushes a commit and immediately tries to merge a PR, the merge operation might read the base branch from a lagged replica that does not have the latest commit. The merge sees a conflict that would not exist if it read from the primary. This is one of those edge cases that only surfaces at scale with minutes of replication lag.
Stale reads create phantom bugs
When replica lag exceeds a few seconds, the symptoms stop looking like "database lag" and start looking like application bugs. Missing records, phantom conflicts, outdated statuses. Support tickets flood in with reports of "random" data inconsistencies. The debugging trap is that each individual report looks like a unique application bug, not a systematic infrastructure issue. Always check replica lag first when you see reports of inconsistent data across different users or sessions.
Representative GitHub-Scale MySQL Architecture
Before GitHub's migration to Vitess, public materials described a large MySQL deployment. The diagram below is a representative model; it explains why replication lag can have broad impact without claiming to reproduce an unpublished production topology.
A few things stand out about this architecture:
Read-heavy workload. GitHub's traffic is overwhelmingly reads: viewing issues, browsing code, loading pull requests. The read-to-write ratio is roughly 10:1 or higher. This is why they use multiple read replicas. The primary cannot handle both writes and the full read load alone.
Asynchronous replication. MySQL replication is asynchronous by default. The primary writes to its binary log and returns success to the client immediately. Replicas pull from the binary log and apply changes at their own pace. This means replicas are always slightly behind the primary. Under normal conditions, the lag is sub-second and invisible to users. Under stress, it can grow to minutes or hours.
ProxySQL as the routing layer. ProxySQL sits between the application and MySQL, routing writes to the primary and reads to replicas. It monitors replica lag and can reroute reads to the primary when lag exceeds a configurable threshold. This is a safety mechanism, but it has a dangerous side effect: when replicas are lagged, the primary absorbs the full read load on top of its write load.
For interviews, this is a textbook read-replica architecture. If you are asked about database scaling, you should be able to draw this diagram from memory and explain the tradeoff: replicas give you read throughput, but replication lag means reads can be stale. The question is always "how stale is acceptable?"
Row-based vs statement-based replication
GitHub uses row-based replication (RBR), where the binary log records the actual row changes rather than the SQL statements. RBR is safer for complex queries but produces larger binary logs for bulk operations. During a schema migration that touches billions of rows, the binary log volume can be enormous, which contributes to replica lag.
Root Cause: Schema Migration on a Hot Table
The specific trigger was a schema migration on one of GitHub's largest and most actively written tables. Think of tables like issues, pull_requests, or repository_events, tables with billions of rows that receive thousands of writes per second.
What the migration looked like:
GitHub uses gh-ost (GitHub Online Schema Trickle) for online schema changes. gh-ost works by:
- Creating a shadow copy of the table with the new schema
- Copying existing rows from the original table to the shadow table in small batches
- Tailing the binary log to capture any writes that happen during the copy
- Applying those captured writes to the shadow table to keep it in sync
- Performing an atomic table swap once the shadow table is fully caught up
On the primary, this is relatively graceful. gh-ost copies rows in small chunks, throttles based on server load, and does not lock the original table for the duration. Writes continue unimpeded. The primary barely notices.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.