~learn.

Side quest 29Code hosting

GitHub.

A Billion Repositories, 124 Million CI Jobs a Day, and a Push That Lands on a Majority or Not at All

EXPERT53 MIN READ6 PHASES · 45 MIN INTERVIEW+120 XP

Mission brief

Design a code hosting platform at GitHub's scale

Quest log0/7 stages · 0/10 checkpoints · 0/8 quick checks
repositories, each stored by Spokes on five replicas by default (GitHub Engineering, October 2026)
1 billion
pushes and 1.3 billion merged pull requests in the year to September 2026 (Octoverse, October 2026)
17.9 billion
GitHub Actions jobs a day, 1,440 a second, on average over the year to September 2026 (Octoverse, October 2026)
124 million
operations a day on repository data through Spokes (GitHub Engineering, October 2026)
Well over 100 billion

Company names and logos are trademarks of their owners, shown only to identify each company. This course isn't affiliated with or endorsed by any of them.

watch it first · newThe whole design in a 16-minute animated lecture4 quiz checkpoints · ask the AI about any moment · captions, chapters and speed
Optional · timed run · 6 phases · 45 min

Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.

Intro · 4 min

How Do You Keep a Billion Repositories Identical on Five Machines Each, While People and Agents Push to Them Around the Clock?

It is a Thursday morning in late October 2026 in Bengaluru. Nandini Hegde maintains an open-source Python library that parses and validates Indian postal addresses and PIN codes. Thousands of projects depend on it. Today is the 4.0 release. At 10:40 she adds the last pull request to the merge queue, which tests it against the newest main together with the two pull requests ahead of it. At 11:02, from her laptop in Koramangala, she runs git push origin v4.0.0 over SSH.

That one tag is a small write with a long tail. It must reach a majority of the repository's replicas before GitHub says yes. It wakes the release workflow: a matrix of 24 jobs on Linux, macOS and Windows, each on a fresh virtual machine, building packages from code anyone could have proposed. It sends webhooks to her documentation host and to the project's chat bot. Within the hour, thousands of other projects' CI pipelines fetch her repository to test against the new version. Her co-maintainer in Pune has three coding agents pushing to their own branches of the same repository every few minutes. And at 11:20 someone searches all of GitHub for parse_pin(, the function she deprecated, to see whose code will break.

GitHub, from published numbers
  • 225 million+ — developers on GitHub; India has 32.0 million, second only to the United States (Octoverse, 28 October 2026)
  • 17.9 billion — pushes in the year to September 2026; 34.3 billion commits; 1.3 billion merged pull requests, about 3.5 million a day (same report)
  • 124 million — Actions jobs a day on average over the year to September 2026, 1,440 a second, more than three times the previous year's total (Octoverse 2026); GitHub gave 71 million a day in December 2025 and about 23 million in early 2024 (GitHub, 11 December 2025)
  • 1 billion — repositories, each kept on five replicas by default; Git activity grew from 218.2 billion to 473.3 billion events a month between September 2025 and August 2026 (Brian Celenza, GitHub Engineering, 6 October 2026)
  • 100 to 1,000 — reads for every write in most repositories; Spokes handles well over 100 billion operations a day (Eleanor White and Victoria Dye, GitHub Engineering, 21 October 2026)
  • Almost 45 million repositories, 115 TB of code and 15.5 billion documents in the new code search at its beta, with ~100 ms p99 per shard (Timothy Clem, GitHub Engineering, February 2023)

For context:
GitHub has published how Spokes replicates repositories, how pushes are processed, how code search works, Actions' volumes and limits, and monthly availability reports with root causes; it hasn't published how the Actions scheduler places jobs or how webhooks are queued internally
GitHub has been moving its infrastructure to Azure since 2025; this quest describes the published state as of October 2026
Everything not in the list above is an illustrative design, not GitHub's internal architecture

This is the system we'll design today.

ExplainerThe whole quest: GitHub

GitHub's Spokes serves a billion repositories, five replicas each by default; the year to September 2026 saw 17.9 billion pushes, 1.3 billion merged pull requests and 124 million Actions jobs a day. That is ~1,300 pushes a second and up to ~1.3 million Git reads a second; each push wakes 60+ follow-up tasks, and on an assumed 5 minutes a job ~430,000 runner VMs are busy. A push is acknowledged only when a strict majority of replicas apply it with matching checksums; in 2026 GitHub is narrowing the quorum from the whole repository to each reference. Everything after the write reads one Kafka event per push, with independent consumers, which raised fully processed pushes from ~99.897% to at least 99.999%; the merge queue tests each pull request against the newest main. Webhook receivers must answer within 10 seconds, so each host gets its own queue, cap and breaker, and a redelivery keeps its delivery id. Actions queues jobs per account, runs each on a fresh VM, gives forks no secrets and refuses stale runners by lease epoch. Code search (Blackbird) is sharded by Git blob id, about 100 ms p99 per shard, where grepping 115 TB would take ~96 seconds. The deep dives end with release day, a receiver down for four days and 2,000 coding agents on one monorepo.

The Interview Begins#

You're interviewing at a company that builds developer tools. The interviewer pastes the end of a terminal session into the shared document: * [new tag] v4.0.0 -> v4.0.0. Next to it, a screenshot of a repository's Actions tab: "Release · 24 jobs · Queued". Under it, a webhook's "Recent deliveries" list, one row red: "Failed · timed out".

Interviewer

"One push did all of that. Design the platform behind it: where the repository lives, how the push gets in, what happens after it, how those 24 jobs run, how the world hears about it, and how anyone finds the code later."

Design a code hosting platform at GitHub's scale

Developers and automated agents push to and fetch from Git repositories over SSH and HTTPS. Every push updates pull requests, triggers CI workflows, sends webhooks to outside services and refreshes a code search index. Repositories range from a single file to monorepos that thousands of engineers push to every day.

Requirements
  • Host Git repositories of any size; accept pushes, serve clones and fetches over SSH and HTTPS
  • Never lose an acknowledged push; once a branch moves, nobody sees it move back
  • Pull requests with diffs, reviews, mergeability checks and a merge queue
  • Deliver a webhook for every subscribed event to millions of outside endpoints
  • Run CI jobs from untrusted code, isolated per job, with fair shares between customers
  • Search code across all public repositories and the private ones a user can see
Interviewer

"Leave out Copilot, Codespaces, Pages, packages and the issue tracker. Billing only matters where it shapes Actions."

Phase 1 · Stage 1 of 7 · 5 min budget · 3 min read

Phase 1: Requirements Clarification (5 minutes)#

You

"A code host is three systems sharing one event: a replicated storage system for Git data, an event pipeline that reacts to every push, and a compute platform that runs strangers' code. I'll ask about each, starting with the write path, because everything hangs off it."

Your Questions#

Transcript
You

"How many repositories, and how busy are they?"

Interviewer

"GitHub wrote this month that Spokes serves a billion repositories. Pushes grew 4.9 times in a year, to 3.35 billion a month, and the busiest repository saw roughly a billion requests in August 2026."

You

"What's the read-to-write ratio?"

Interviewer

"In 2017 GitHub wrote that reads outnumber writes about 100 to 1. In October 2026 it said most repositories see between 100 and 1,000 reads for every write. CI and code scanning clone or fetch the same branch tip thousands of times a minute."

You

"What does 'the push succeeded' promise?"

Interviewer

"That's your call, but here's GitHub's: Spokes doesn't accept a write unless a strict majority of replicas can apply it and get the same result, and always at least two."

You

"What happens after a push?"

Interviewer

"In 2024 GitHub counted over 60 pieces of logic, owned by 20 services, that run on every push: pull request sync, push webhooks, workflow triggers, Pages and more."

You

"And the CI side?"

Interviewer

"Actions ran 124 million jobs a day over the past year. Each GitHub-hosted runner is a new virtual machine, except single-CPU runners, which run in a container on a shared VM. A job can run for up to 6 hours. Standard runners are free for public repositories; a Linux 2-core minute lists at $0.006."

You

"Webhooks?"

Interviewer

"Receivers must answer with a 2XX within 10 seconds or the delivery counts as failed. GitHub doesn't redeliver failed deliveries automatically; receivers can redeliver anything from the past 3 days. Payloads are capped at 25 MB."

Functional Requirements#

Functional requirements
  • Git: push, clone and fetch over SSH and HTTPS; branches, tags, force pushes and deletes; partial and shallow clones
  • Pull requests: diffs against the base, review comments, mergeability ("can this merge cleanly?"), a merge queue that tests changes against the newest base
  • Events: every push, pull request, review and CI state change becomes an event that updates the UI and triggers workflows and webhooks
  • Webhooks: signed deliveries to endpoints that subscribe, a unique delivery id, a delivery log, manual and API redelivery
  • Actions: workflows triggered by events, a job queue per customer, hosted runners (one VM per job) and self-hosted runners, logs, caches and artifacts
  • Search: substring, regex and symbol search over code, filtered by language, owner and path, showing only what the user may see

Non-Functional Requirements#

Non-functional requirements
Scale (published, then estimated)
  • ~1,300 pushes a second on average (3.35 billion a month, published); design for ~5,000 at peak
  • 130,000 to 1.3 million Git reads a second (100 to 1,000 reads per write)
  • ~1,440 CI jobs a second started on average (published); design for ~5,000
Latency (design targets)
  • Push acknowledged in < 1 s at p99 for ordinary repositories, even with replicas in different regions
  • Push to webhook sent and push to CI job queued in < 10 s at p99; hosted job started in < 30 s at p50
  • Code search answered in < 1 s at p99
Durability and isolation
  • Zero lost acknowledged pushes; a reference never goes back to an older value for a reader
  • Zero hosted runners reused between jobs; one customer's burst never stops another customer's work

Phase 2 · Stage 2 of 7 · 5 min budget · 4 min read

Phase 2: Back of the Envelope Estimation (5 minutes)#

You

"The published anchors are strong here: pushes a month, reads per write, Spokes operations a day, Actions jobs a day and code search's index. Storage sizes, job durations and webhook counts aren't published, so those are my assumptions, labelled."

Your turn · checkpoint 1not answered

Before the numbers: what does one push cost?

GitHub received 3.35 billion pushes in a month in 2026. How many is that a second? Most repositories see 100 to 1,000 reads for every write: how many Git reads a second does that imply, and does it agree with Spokes' "well over 100 billion operations a day"? And if every push triggers over 60 pieces of follow-up logic, how many background tasks a second is that?

Think it through, or jot it down. Then compare.

One strong answer

  • Pushes: 3.35 billion ÷ (30 × 86,400 s) ≈ 1,300 a second on average. Design for a peak of ~3× plus growth: ~5,000 a second.
  • Reads: 1,300 × 100 to 1,000 = 130,000 to 1.3 million a second. Spokes' 100 billion operations a day ÷ 86,400 ≈ 1.2 million a second: the upper end, which also counts internal reads by the web app, pull requests and CI.
  • Follow-up tasks: 1,300 × 60 ≈ 78,000 background tasks a second before any CI job runs.
  • So the write rate is modest; what makes a push expensive is that each one needs agreement among replicas, then fans out into dozens of tasks and hundreds of reads.

Git Traffic#

WorksheetPushes and reads (published rates, one assumed peak)
Value
Pushes a month (GitHub, 2026) 3.35 billion
Average push rate ~1,300 a second
Peak (assumed 3× average, plus growth) ~5,000 a second
Reads per write (GitHub, 2026) 100 to 1,000
Git reads 130,000 to 1.3 million a second
Spokes operations (GitHub, 2026) 100 billion+ a day ≈ 1.2 million a second
Merged pull requests (Octoverse 2026) 3.5 million a day ≈ ~40 a second

Conclusion: reads outnumber writes by two to three orders of magnitude, so read capacity must scale without slowing writes. Writes are few, but each one is a small distributed transaction.

Storage#

WorksheetRepository storage (every size here is an assumption: GitHub doesn't publish its total)
  • Repositories: ~1 billion (published); most are tiny, a few monorepos are hundreds of gigabytes
  • Average packed size: assume ~20 MB → ~20 PB of Git data
  • Replicas: five by default (published) → ~100 PB on fileservers
  • Search: in 2023, 115 TB of searchable code shrank to 28 TB of unique content after deduplication, and the whole index, content included, was 25 TB (published)

Conclusion: the copies are the cost; content duplicated across forks and vendored folders is the opportunity. Git's content addressing lets search and storage deduplicate for free.

Actions#

WorksheetCI compute (the job rate is published; durations and boot times are assumptions)
Value
Jobs a day (Octoverse 2026) 124 million (~1,440 a second)
Average job length (assumed) ~5 minutes
Runner-minutes a day ~620 million
Runners busy at once, on average 620 million ÷ 1,440 minutes ≈ ~430,000 VMs
Peak (assumed 2× average) ~860,000 VMs
VM boot if started on demand (assumed) ~30 s → 10% of a 5-minute job
List price, Linux 2-core (GitHub docs) $0.006 a minute

Conclusion: at list price, a day's runner-minutes would be worth ~$3.7 million, so a few percent of boot time, idle warm VMs or retried jobs is real money. Starting from a warm pool and deleting each VM after one job is the design's core trade between isolation and cost.

Webhooks and Events#

WorksheetEvents out (GitHub doesn't publish delivery counts: these are assumptions)
  • Events: pushes, pull request activity, reviews, check runs and workflow jobs: assume ~1 billion a day
  • Subscribed hooks per event: assume ~2 → ~2 billion deliveries a day ≈ 23,000 a second, ~70,000 at peak
  • Slow receivers: if 5% of deliveries go to endpoints that hang until the 10-second timeout, ~11,500 connections are held open at any moment just waiting

Conclusion: the receivers, not GitHub, set the pace of delivery. Isolation per destination, and a decision about how long a delivery stays worth sending, matter more than raw throughput.

Key Metrics Summary#

Metric Value
Repositories (GitHub, 2026) ~1 billion, five replicas by default
Pushes (GitHub, 2026 / design peak) ~1,300 a second / ~5,000 a second
Git reads (100 to 1,000 per write) 130,000 to 1.3 million a second
Actions jobs (Octoverse 2026) 124 million a day, ~1,440 a second
Busy runner VMs (assumed 5-minute jobs) ~430,000 average, ~860,000 peak
Code search (GitHub, 2023 beta) 15.5 billion documents, 25 TB index, ~100 ms p99 per shard
Quick check3 questions · +5 XP each

Requirements and estimation

  1. Q1/3 GitHub received 3.35 billion pushes in a month. Roughly how many a second?

    Answer & explanation

    Answer: B

    3.35 billion ÷ 2.6 million seconds in a month ≈ 1,300 a second.

  2. Q2/3 Most repositories see 100 to 1,000 reads per write. What does that mean for the design?

    Answer & explanation

    Answer: B

    130,000 to 1.3 million reads a second against ~1,300 pushes. Adding read capacity must not add work to every push.

  3. Q3/3 124 million CI jobs a day averaging an assumed 5 minutes each keeps roughly how many runner VMs busy at once?

    Answer & explanation

    Answer: C

    620 million runner-minutes ÷ 1,440 minutes in a day ≈ 430,000.

Phase 3 · Stage 3 of 7 · 10 min budget · 4 min read

Phase 3: High-Level Design (10 minutes)#

Your turn · checkpoint 2not answered

Sketch it first: Nandini's tag push

Nandini runs git push origin v4.0.0 over SSH. Which component checks her key, and how does the push find her repository's replicas? What has to be true before Git prints [new tag] on her screen? Then: what turns the new tag into 24 CI jobs and two webhooks, and why shouldn't one slow webhook receiver delay her CI? Finally, where do the 24 jobs run, and what stops a malicious pull request's code from seeing her release secrets?

Think it through, or jot it down. Then compare.

One strong answer

  • The front door: a layer-4 load balancer spreads connections across Git proxies; the proxy checks her key and write permission and asks Spokes which fileservers hold the repository.
  • Before "yes": the new objects go to the replicas, then the update to refs/tags/v4.0.0 goes through a vote; only when a majority of the five apply it with matching checksums is the push acknowledged.
  • After "yes": one push event goes to Kafka; independent consumers run pull request sync, webhooks, workflow triggers and indexing as separate jobs with their own retries.
  • CI: 24 jobs enter her account's queue; a scheduler hands each to a fresh VM from a warm pool, with a token for this repository only; the VM is destroyed afterwards.
  • Webhooks: each destination has its own queue and concurrency limit, so a receiver that hangs for 10 seconds only slows its own deliveries.
  • Secrets: workflows triggered by pull requests from forks get no repository secrets and a read-only token; release secrets are only available to the tag-triggered workflow.
You

"Three planes around one event. The storage plane decides, by vote, whether a write happened. The event plane turns each accepted write into isolated follow-up jobs. The compute plane runs CI on disposable machines. None waits for the next."

System Architecture#

DiagramA code host: vote on the write, fan out the event, run CI on disposable machines
%% title: A code host: vote on the write, fan out the event, run CI on disposable machines
flowchart TD
  subgraph L1["Layer 1 · Clients"]
    direction LR
    DEV@{ icon: "sd:laptop", label: "Developers and agents<br/>git over SSH or HTTPS" }
    WEB@{ icon: "sd:browser", label: "Web and API<br/>pull requests, search" }
  end
  subgraph L2["Layer 2 · Edge"]
    direction LR
    LB@{ icon: "sd:load-balancer", label: "GLB, layer 4<br/>one IP, many proxies" }
    GITP["Git proxies<br/>SSH and HTTPS, auth"]:::service
    APP["Rails monolith<br/>web, API, PRs"]:::service
  end
  subgraph L3["Layer 3 · Git storage"]
    direction LR
    SPK["Spokes<br/>route, vote, repair"]:::hot
    FS@{ icon: "sd:servers", label: "Fileservers<br/>five replicas, several sites" }
    BLOB@{ icon: "sd:object-storage", label: "Azure Blob Storage<br/>virtual replica" }
  end
  subgraph L4["Layer 4 · Metadata and events"]
    direction LR
    MY@{ icon: "tech:mysql", label: "MySQL and Vitess<br/>PRs, issues, users" }
    K@{ icon: "tech:kafka", label: "Kafka<br/>one event per push" }
    JOBS@{ icon: "sd:worker", label: "Background jobs<br/>60+ push tasks" }
  end
  subgraph L5["Layer 5 · Outward"]
    direction LR
    HOOK@{ icon: "sd:webhook", label: "Webhook delivery<br/>per-endpoint queues" }
    ACT["Actions control plane<br/>queues, fair share"]:::core
    RUN@{ icon: "sd:server", label: "Runner VMs<br/>one job each" }
    SRCH@{ icon: "sd:search", label: "Code search<br/>sharded by blob" }
  end
  DEV ==> LB
  WEB ==> LB
  LB ==> GITP
  LB --> APP
  GITP ==> SPK
  APP --> SPK
  APP --> MY
  SPK ==> FS
  SPK -.->|"durable copy"| BLOB
  SPK ==>|"ref updated"| K
  K ==> JOBS
  JOBS ==> HOOK
  JOBS ==> ACT
  ACT ==> RUN
  RUN -.->|"clone"| GITP
  K --> SRCH
  class DEV,WEB client
  class LB edge
  class FS,BLOB,MY store
  class K queue
  class JOBS,RUN,HOOK,SRCH service

KEY INSIGHTS:

  • Git data and metadata live in different systems. Repositories are files on fileservers replicated by Spokes; pull requests, issues, users and permissions are rows in MySQL. They fail differently and are replicated differently
  • The write is a vote, the rest is an event. Only the reference update needs agreement among replicas. Everything that follows (pull requests, webhooks, CI, search) reads from one event and can lag, retry or fail on its own
  • Strangers' code runs on machines that are thrown away. The compute plane is multi-tenant by design, so isolation is per job, not per customer

Data Flow: Nandini Pushes the Tag#

DiagramOne tag push, from Nandini's laptop to a majority of replicas and onwards
%% title: One tag push, from Nandini's laptop to a majority of replicas and onwards
sequenceDiagram
  autonumber
  participant N as Nandini's laptop
  participant P as Git proxy, SSH
  participant S as Spokes
  participant R as Five replicas
  participant K as Kafka
  participant J as Push jobs
  N->>P: git push over SSH, key checked
  P->>S: Which replicas hold this repository
  S-->>P: Five replicas, preference order
  P->>R: Send pack, new objects only
  S->>R: Vote on ref update, old and new value
  R-->>S: Locked, applied, checksums match on 5 of 5
  S->>R: Commit
  S-->>P: Committed on a majority
  P-->>N: new tag v4.0.0
  S->>K: Push event, repo, ref, old and new SHA
  K->>J: Fan out to independent consumers
  J->>J: PR sync, webhooks, workflow trigger, search

MEANWHILE, IN PARALLEL:

  • The release workflow's 24 jobs enter the Actions queue of Nandini's organization; warm VMs pick them up and clone the tag through the same Git proxies
  • Two webhook deliveries go into the queues for her documentation host and her chat bot; each is signed and carries a delivery id
Quick check3 questions · +5 XP each

High-level design

  1. Q1/3 Nandini's push reaches 3 of 5 replicas; 2 time out. Is the push acknowledged?

    Answer & explanation

    Answer: A

    Spokes accepts a write when a strict majority applies it identically; losers of the vote are repaired from the others.

  2. Q2/3 Why is the push event published to Kafka instead of running all follow-up work inside the push?

    Answer & explanation

    Answer: B

    One event, many independent consumers: a smaller blast radius and per-task retry rules.

  3. Q3/3 Where do pull requests and issues live, compared with Git data?

    Answer & explanation

    Answer: B

    Git objects and references are on fileservers; metadata (pull requests, issues, users) is in MySQL.

Phase 4 · Stage 4 of 7 · 20 min budget · 26 min read

Phase 4: Deep Dives (20 minutes)#

Deep Dive 1: Storing a Billion Repositories (Week 1 Concepts: Replication Trade-offs; Week 5: Consistency Models, Leader Election)#

You

"A Git repository is two things: a pile of immutable objects named by their hashes, and a small set of references (branch and tag names) that point at them. Objects never conflict; two copies of the same object are the same bytes. References are the only mutable state, so they're the only thing replicas must agree on."

This builds on Week 1, Day 2 · Replication Trade-offs, Week 5, Day 1 · Consistency Models and Week 5, Day 5 · Leader Election.

Your turn · checkpoint 3not answered

Five copies, one branch, two writers

Nandini's repository has five replicas. At the same moment, her co-maintainer deletes the branch release-3.x and an agent pushes a commit to it. One replica is busy and answers slowly; one is in another region, 25 ms away. Which write wins, and how do all five replicas end up agreeing on that? How many replicas must apply a write before you say "pushed"? Why not do what most databases do: one primary per repository that orders writes, with the others copying it?

Think it through, or jot it down. Then compare.

One strong answer

  • Serialize writes on a majority. Each write first takes a lock on a majority of the replicas. Two writes can't both hold a majority, so one goes first; the second then sees the branch's new value and is checked against it (the agent's push expects the old value, finds the branch gone, and is rejected).
  • "Pushed" means a strict majority applied it and got the same result: 3 of 5, never fewer than 2. Replicas that disagree or time out lose the vote, are marked unhealthy and repaired from the others.
  • No primary to fail over, at a price. With a primary, losing it means an election, and a bad one leaves two copies that each lack the other's writes. A vote per write removes the election, but every write waits on its replicas, so the slowest or most distant one sets write latency.

What is published: GitHub announced DGit in April 2016: three copies of every repository on three servers, with a write "only committed if at least two replicas confirm success". In September 2016 Patrick Reynolds renamed it Spokes: copies sit in different racks; no update is accepted "unless a strict majority of the replicas can apply the change and get the same result"; every write "acquires an exclusive lock on a majority of replicas", so "Spokes eliminates conflicts by eliminating concurrent writes entirely"; and rather than electing a primary, Spokes "treats every write as an election". Losers of a vote are marked unhealthy until repaired; on a typical day "a few dozen writes" aren't unanimous. In 2017 Michael Haggerty described a three-phase commit, with the replicas also used as a distributed lock, costing four round trips to distant replicas (60 to 80 ms each across the US) and about a third of a second per reference update, and a checksum per replica built from the XOR of a hash of every (reference, value) pair, so it can be updated incrementally.

DiagramOne reference update as a vote across five replicas
%% title: One reference update as a vote across five replicas
flowchart TD
  P["Push arrives<br/>pack already on replicas"]:::service
  LOCK["Phase 1 · lock<br/>repository or reference on each replica"]:::core
  CHECK{"Old value matches<br/>and new value valid?"}
  APPLY["Phase 2 · apply tentatively<br/>compute new checksum"]:::core
  VOTE{"Strict majority<br/>with equal checksums?"}
  COMMIT(["Phase 3 · commit<br/>acknowledge the push"]):::ok
  ROLL(["Roll back everywhere<br/>reject the push"]):::bad
  REPAIR@{ icon: "sd:worker", label: "Sweeper<br/>repair the losers" }
  P ==> LOCK ==> CHECK
  CHECK ==>|"yes"| APPLY ==> VOTE
  CHECK -->|"no"| ROLL
  VOTE ==>|"yes"| COMMIT
  VOTE -->|"no"| ROLL
  COMMIT -.->|"minority disagreed"| REPAIR
  class REPAIR service
  • Majority, not "two copies". Two copies survive one failure, but conflicting writes must be applied in the same order everywhere, and a majority lock makes concurrent writers impossible. With five replicas, a write needs three.
  • Failure detection from real traffic. Spokes marks a fileserver offline after three requests fail in a row, and each application server keeps its own view, so one confused app server can't take a healthy fileserver out for everyone. Offline only moves a replica to the end of the read list.
  • Repair is N-to-N, across racks. When a server dies, new copies of its repositories are made from any surviving replica onto any server, never two in one rack, so the bigger the fleet, the faster the repair. By 2021 GitHub kept five replicas "across our three data centers".

Compare: the database that did fail over#

The 43-second partition in October 2018 hit GitHub's MySQL clusters, which hold "non-Git metadata" such as pull requests and issues. Each had one primary, and Orchestrator promoted West Coast primaries during a 43-second network cut; the East still held a few seconds of writes the West never got, and service was degraded for 24 hours and 11 minutes. GitHub's first fix was to forbid promotions across regions. A single-primary design must decide where it may fail over to; a per-write majority never fails over, it just refuses writes when it can't reach a majority.

2026: one copy in another region, one in object storage#

What is published: In October 2026 GitHub wrote that a copy of 99.99999% of repositories now lives in Azure; most repositories hold two of their five replicas there and Azure serves about half of Git traffic, with reads sent to a replica in the same region and zone. But the quorum "waits for every healthy replica to respond (up to a timeout)" and each write "holds a repository-wide lock until it commits", so the slowest replica sets latency and throughput, and an Azure replica is about 25 ms away. GitHub's fix, now rolling out: change the quorum's scope "from the whole repository to individual references", since "Git's consistency requirements apply to each reference individually"; in internal testing with realistic traffic, writes to different references "consistently saw 3–4x higher throughput at lower latency". The next step makes Azure Blob Storage a member of the quorum, a "virtual replica", so a write isn't acknowledged until it's durable in object storage; read replicas then warm their caches from it without joining the quorum. In its 6 October post GitHub said its new architecture "has delivered up to 35 times higher write throughput" in internal benchmarks.

  • Per-reference serialization keeps Git's guarantee (no reader sees a branch go back) while agents pushing to their own branches proceed in parallel; storage apart from compute fixes the old trap, where adding a replica for reads added a voter to every write.
pythonh · 36 lines
# spokes/ref_update.py

"""
A reference update as a majority vote (illustrative, after Spokes' three-phase
commit), locked per reference as in GitHub's 2026 design. The checksum is the XOR
of hash(refname, value) over all references, so an update changes it by two hashes.
"""

import hashlib

def h(ref: str, sha: str) -> int:
    return int.from_bytes(hashlib.sha256(f"{ref}\0{sha}".encode()).digest()[:8], "big")


def new_checksum(old: int, ref: str, old_sha: str, new_sha: str) -> int:
    return old ^ h(ref, old_sha) ^ h(ref, new_sha)


def update_ref(replicas, ref: str, old_sha: str, new_sha: str, timeout_s: float = 0.5) -> bool:
    need = len(replicas) // 2 + 1                       # 3 of 5
    locked = [r for r in replicas if r.lock(ref, timeout_s)]
    if len(locked) < need:
        for r in locked: r.unlock(ref)
        return False                                    # no majority: refuse, never diverge
    votes = {}
    for r in locked:                                    # phase 2: prepare
        ok, checksum = r.prepare(ref, old_sha, new_sha)   # checks old value, applies tentatively
        if ok:
            votes.setdefault(checksum, []).append(r)
    winners = max(votes.values(), key=len, default=[])
    if len(winners) >= need:                            # phase 3: commit
        for r in winners: r.commit(ref)
        for r in set(locked) - set(winners): r.rollback(ref); r.mark_unhealthy()
        return True
    for r in locked: r.rollback(ref)
    return False

When one repository is the whole company#

Your turn · checkpoint 4not answered

A monorepo that won't take pushes

A customer's monorepo has thousands of engineers, hundreds of thousands of references and a push every few seconds. Pushes start failing. You find three things: background maintenance (repacking objects into packfiles) keeps hitting its time limit; the checksum check holds the write lock for 20 to 30 seconds whenever a replica is repaired; and a per-server cap on push concurrency, added years ago to protect other tenants, is queueing pushes. What do you change?

Think it through, or jot it down. Then compare.

One strong answer

  • Make maintenance proportional to new data: keep packs in a geometric progression (each at least twice the next) and merge only the small, recent ones; repack fully now and then; retry failures within hours.
  • Move expensive work out of the lock. Compute the replicas' checksums before taking the lock, so the lock covers only the comparison and the update.
  • Revisit old limits: a tenant-protection cap built for older hardware may now only hurt; raise it gradually while watching neighbours.
  • For agent-heavy repositories, per-reference locking lets pushes to different branches proceed in parallel; only merges into the same branch must take turns.

What is published: In March 2021 GitHub described Project Cyclops, after its largest monorepo customers and its own github/github saw push failures. Maintenance runs "after every 50 git push operations, or after we receive 40MB of unpacked files"; limiting expensive delta comparisons in git repack "eliminated nearly all" maintenance failures; failed maintenance was retried every four hours instead of after seven days; an old per-fileserver push cap was removed; and replica checksums, which took 20 to 30 seconds inside the lock during repairs, were precomputed, cutting the lock "to under 1 second". In April 2021 Taylor Blau described geometric repacking with multi-pack reachability bitmaps, which cut average repack time "from around 1 minute... to just 15 seconds". Bitmaps let a server work out what to send by bitwise operations: walking the Linux kernel's objects fell from over 33 seconds to 1.57.

Deep Dive 2: The Front Door: Push, Clone and Fetch over SSH and HTTPS (Week 4 Concepts: Multi-Tier Caching; Week 1: Rate Limiting; Week 2: Timeouts)#

You

"In Git's protocol the server advertises its references, the client says what it wants and has, and the server builds a pack of exactly the missing objects. The front door makes that cheap, routes it to a good replica, and keeps long transfers alive."

This builds on Week 4, Day 5 · Multi-Tier Caching, Week 1, Day 3 · Rate Limiting at Scale and Week 2, Day 1 · Timeout Hell.

Your turn · checkpoint 5not answered

Ten thousand pipelines fetch the tag

Within an hour of Nandini's release, thousands of other projects' CI jobs fetch her repository, most of them full clones of the same commit, many more than once a minute. Her own release jobs clone it too. Meanwhile a contributor in Chennai is halfway through a 40-minute clone of a large monorepo on a slow connection when the fileserver holding his replica needs a kernel update. How do you serve the burst cheaply, and how do you take that fileserver down without breaking his clone?

Think it through, or jot it down. Then compare.

One strong answer

  • Spread reads over every in-sync replica, nearest first. All five copies (and, in the new design, cache-only read replicas) can answer; the writes don't care how many readers there are.
  • Make the expensive part cheap: reachability bitmaps turn "which objects does this client need?" into bitwise operations, and a pack built for a popular commit can be cached for the next identical request (our design; GitHub hasn't published a pack cache).
  • Ask CI to fetch less, and rate-limit: blobless or shallow clones transfer a fraction of the data; per-client and per-repository limits stop one runaway pipeline.
  • Drain, don't kill: mark the fileserver offline for new reads but let running clones finish (quiescing), keep sending it writes so it stays in sync, then reboot. His clone completes; the next one goes elsewhere.

What is published: GitHub's GLB (2018) "powers the majority of GitHub's public web and git traffic": a layer-4 load balancer with stateless directors that pick a primary and a secondary proxy per connection, so directors can change without breaking open connections. Spokes returns "up-to-date replicas, sorted in preference order" and routes reads "to the closest replica that is in sync". Before planned reboots, fileservers are quiesced: no new reads, but "existing read operations, including clones, are allowed to finish", which "can take anywhere from a few seconds to many hours". A 2020 GitHub post by Derrick Stolee recommends blobless clones for developers and long-lived build machines, and shallow clones only for builds deleted after one use. On 9 February 2026 a burst of cache rewrites caused "connection exhaustion in the service proxying Git operations over HTTPS"; HTTPS pushes and pulls failed while "SSH-based Git operations were not affected".

DiagramA fetch, from the edge to the nearest in-sync replica
%% title: A fetch, from the edge to the nearest in-sync replica
flowchart TD
  C@{ icon: "sd:terminal", label: "git fetch<br/>SSH or HTTPS" }
  LB@{ icon: "sd:load-balancer", label: "GLB<br/>stateless directors" }
  PX["Git proxy<br/>auth, rate limits"]:::service
  RT{"Spokes route<br/>in-sync replicas, nearest first"}
  PC@{ icon: "sd:cache", label: "Pack cache<br/>hot commits, our design" }
  FS1@{ icon: "sd:server", label: "Replica, same zone<br/>bitmaps answer what to send" }
  FS2@{ icon: "sd:server", label: "Next replica<br/>after 3 failures in a row" }
  C ==> LB ==> PX ==> RT
  RT ==>|"popular tip"| PC
  RT ==>|"first choice"| FS1
  RT -.->|"failover"| FS2
  class C client
  class LB edge
  class PC cache
  class FS1,FS2 store
  • Two protocols, two proxy fleets (our design). GitHub's February 2026 report names a separate "Git HTTPS proxy" layer, and SSH Git kept working. Here SSH and HTTPS share replica routing but run on separate proxy fleets, so one can fail without the other.
  • Authenticate, then authorize per repository (read, write, protected branch) before Spokes is asked anything.
  • Timeouts sized to the operation. A reference advertisement takes milliseconds; a clone can stream for an hour. One idle timeout for both either kills big clones or lets stuck connections pile up.
  • Shed by cost. A full clone of a monorepo isn't a git ls-remote; when fileservers saturate, admit cheap operations and shed the most expensive first.
pythonread_order · 27 lines
# gitproxy/route.py

"""
Picking a replica for a read (illustrative, after Spokes' published rules): in-sync
replicas nearest first; three failures in a row moves a replica to the end of the
list, not off it; a quiescing replica takes no new reads.
"""

FAILS_TO_DEMOTE = 3


def read_order(replicas: list, fail_streak: dict, draining: set) -> list:
    usable = [r for r in replicas if r.in_sync and r.name not in draining]
    healthy = [r for r in usable if fail_streak.get(r.name, 0) < FAILS_TO_DEMOTE]
    suspect = [r for r in usable if fail_streak.get(r.name, 0) >= FAILS_TO_DEMOTE]
    return sorted(healthy, key=lambda r: r.distance) + suspect    # try suspects last, not never


def serve_fetch(repo, replicas, fail_streak, draining, request):
    for r in read_order(replicas, fail_streak, draining):
        try:
            pack = r.upload_pack(repo, request, timeout_s=request.budget_s)
            fail_streak[r.name] = 0
            return pack
        except (ConnectionError, TimeoutError):
            fail_streak[r.name] = fail_streak.get(r.name, 0) + 1
    raise RuntimeError("no replica could serve the fetch")
Quick check3 questions · +5 XP each

Storage and the front door

  1. Q1/3 Why does Spokes lock a majority of replicas for each write rather than just copying to two?

    Answer & explanation

    Answer: B

    Two copies survive one failure; the majority lock is what serializes conflicting writes.

  2. Q2/3 In GitHub's 2018 incident, MySQL primaries were promoted across regions. Why can't the same thing happen to a Spokes repository?

    Answer & explanation

    Answer: B

    A per-write majority stops accepting writes rather than electing a new writer in the wrong place.

  3. Q3/3 Why is GitHub moving to per-reference serialization in 2026?

    Answer & explanation

    Answer: B

    Git's guarantees are per reference, so only writes to the same reference must be ordered.

Deep Dive 3: After the Push: Pull Requests, Test Merges and the Merge Queue (Week 3 Concepts: Queue vs Stream, Dead Letters; Week 1: Hot Keys)#

You

"Once the push is acknowledged, dozens of things must happen with different deadlines and retry rules. A webhook an hour late is useless; a push record an hour late is fine. In one job, they share each other's worst failure modes."

This builds on Week 3, Day 1 · Queue vs Stream, Week 3, Day 4 · Dead Letters and Poison Pills and Week 1, Day 4 · Hot Keys and Skew.

Your turn · checkpoint 6not answered

One push to main, two hundred open pull requests

Someone merges into main of a busy repository with 200 open pull requests targeting it. Every one of those pull requests now shows a stale "can be merged" badge and a stale diff. Meanwhile the push-processing job that should update them crashes halfway, after sending the push webhook but before syncing the pull requests. What should recompute, when, and how do you retry without sending the webhook twice? Then: 40 engineers want to merge into main in the next hour, and each merge must pass CI against the main that includes everyone ahead of them. How do you avoid testing against a main that's already stale?

Think it through, or jot it down. Then compare.

One strong answer

  • One event, many consumers, each with its own retry policy. Pull request sync retries freely; the push webhook is sent once, by its own job, keyed by the event id.
  • Mergeability is derived: a test merge of each pull request with the new main, stored as a hidden reference. 200 of them are bookkeeping writes: coalesce them, put user pushes first, and compute first for the pull requests people are looking at.
  • The merge queue tests each pull request on a temporary branch of the newest main plus everything ahead of it; it merges when that passes, and a failure is removed and the rest re-tested.

What is published: In June 2024 Will Haltom described how one huge background job, RepositoryPushJob, ran every piece of push processing in sequence. Retries were avoided because some steps tolerate duplicates and others don't ("it is also not desirable to dispatch multiples of the same webhook"), and an error in the Pushes MySQL cluster at the start could stop pull requests syncing. GitHub replaced it with a Kafka event per push, fanned out by independent consumers to jobs grouped by owner and retry needs, with a publisher "that would retry until broker acknowledgement". Ownership spread to 15+ teams, and fully processed pushes rose from about 99.897% to at least 99.999%. In 2017 GitHub explained that each push to a pull request branch creates "a test merge and test rebase", and a push to master does so "for every active pull request whose target is master", sometimes "more than a hundred references", keeping a repository busy for about 30 seconds; it coalesced these updates and gave user pushes priority.

DiagramOne push event, many isolated consumers
%% title: One push event, many isolated consumers
flowchart TD
  S["Spokes<br/>ref updated"]:::hot
  K@{ icon: "tech:kafka", label: "Kafka<br/>push event, reliable publisher" }
  PR@{ icon: "sd:worker", label: "PR sync<br/>test merges, retry freely" }
  WH@{ icon: "sd:webhook", label: "Push webhooks<br/>at most once, time-bound" }
  WF@{ icon: "sd:worker", label: "Workflow trigger<br/>keyed by event id" }
  IDX@{ icon: "sd:index", label: "Search indexing<br/>new blobs only" }
  DB@{ icon: "tech:mysql", label: "Push records<br/>idempotent rows" }
  DLQ["Dead letters<br/>alert the owner"]:::bad
  S ==> K
  K ==> PR
  K ==> WH
  K ==> WF
  K --> IDX
  K --> DB
  PR -.->|"retries exhausted"| DLQ
  class K queue
  class PR,WF,IDX service
  class WH service
  class DB store
  • Group by retry semantics. Tasks that tolerate duplicates retry for hours; must-happen-once tasks are keyed by the event id; time-bound tasks give up after a deadline.
  • Each consumer keeps its own offset, so search indexing an hour behind doesn't delay pull request sync. A task that exhausts its retries goes to a dead-letter queue with an owner and an alert, not into a "catch everything" block.
  • Merges contend on one reference. GitHub wrote in 2026 that merge queues and release trains "funnel all that work onto a single ref", and pull request merges grew to nearly 4 times their volume of a year earlier. A busy main is a hot key that per-reference locking can't split.
  • The merge queue turns contention into batches. GitHub's docs describe temporary gh-readonly-queue/{base_branch} branches, a merge_group event for CI, and a build concurrency of 1 to 100 groups. GitHub's own monorepo merges about 2,500 pull requests a month through it, with a 33% shorter average wait (March 2024).
  • Degrade, don't fail. By October 2026, half of sampled pull request traffic could "continue with limited functionality when a shared dependency fails".
pythonhandle · 34 lines
# push/consumers.py

"""
Push follow-up work as independent consumers (illustrative): each task group has
its own retry policy; once-only tasks are keyed by the event id, and time-bound
tasks give up when the event is too old to be useful.
"""

import time

POLICIES = {
    "pr_sync":       {"max_attempts": 20, "deadline_s": 6 * 3600, "once": False},
    "push_webhooks": {"max_attempts": 3,  "deadline_s": 10 * 60,  "once": True},
    "workflows":     {"max_attempts": 10, "deadline_s": 3600,     "once": True},
    "push_records":  {"max_attempts": 50, "deadline_s": 24 * 3600, "once": False},
}


def handle(task: str, event: dict, attempt: int, done_keys, run, dead_letter, retry_later):
    p = POLICIES[task]
    key = f"{task}:{event['id']}"
    if p["once"] and done_keys.contains(key):
        return                                          # already done by an earlier attempt
    if time.time() - event["pushed_at"] > p["deadline_s"]:
        return dead_letter(task, event, reason="too old to be useful")
    try:
        run(task, event)
        if p["once"]:
            done_keys.add(key)
    except Exception as err:
        if attempt + 1 >= p["max_attempts"]:
            dead_letter(task, event, reason=repr(err))
        else:
            retry_later(task, event, attempt + 1, backoff_s=min(2 ** attempt, 300))

Deep Dive 4: Webhooks to the World (Week 2 Concepts: Webhook Delivery, Idempotency, Circuit Breakers)#

You

"A webhook is a promise to a server we don't run, which may be fast, slow or down for a week. Deliver quickly to healthy receivers, never let an unhealthy one slow the rest, and give receivers what they need to handle duplicates and gaps."

This builds on Week 2, Day 4 · Webhook Delivery System, Week 2, Day 2 · Idempotency in Practice and Week 2, Day 3 · Circuit Breakers.

Your turn · checkpoint 7not answered

A receiver that answers in 40 seconds

Nandini's documentation host does a full rebuild before replying to each push webhook, which takes 40 seconds. Her chat bot replies in 50 ms. A large CI vendor that receives 5% of all deliveries is down for four hours. What happens to each delivery, and what must not happen to everyone else's? When the vendor comes back, should you replay four hours of events to it? And how does any receiver tell a retried delivery from a new event?

Think it through, or jot it down. Then compare.

One strong answer

  • The docs host: at 10 seconds the delivery is cut off and recorded as failed. The fix is on the receiver: answer 2XX at once, then rebuild from a queue.
  • Isolation: each destination has its own queue and concurrency cap; a breaker stops calling the dead vendor and probes it now and then, so it can't hold thousands of connections.
  • Replay is a choice: a backlog dumped on a recovering receiver can knock it over again. Give receivers a delivery log and a redelivery API, and bound how long anything stays queued.
  • Duplicates: a delivery id that stays the same on redelivery lets receivers skip repeats; an HMAC-SHA256 signature lets them reject forgeries.

What is published: GitHub's docs ask receivers to "respond with a 2XX response within 10 seconds", or "GitHub terminates the connection and considers the delivery a failure", and suggest a queue to process later. "GitHub does not automatically redeliver failed webhook deliveries"; receivers can redeliver from the past 3 days. The X-GitHub-Delivery GUID "will be the same as in the original delivery" on redelivery; X-Hub-Signature-256 is an HMAC-SHA256 of the body; payloads over 25 MB aren't delivered. After the 2018 partition, "over five million hook events" were queued; GitHub weighed recovery against "potentially overloading our ecosystem partners", and ~200,000 payloads "that had outlived an internal TTL" were dropped.

DiagramDeliveries isolated per destination
%% title: Deliveries isolated per destination
flowchart TD
  EV@{ icon: "tech:kafka", label: "Events<br/>push, PR, workflow" }
  FAN["Fan-out<br/>subscriptions, sign, delivery id"]:::core
  Q1@{ icon: "sd:queue", label: "Queue · docs host<br/>4 in flight" }
  Q2@{ icon: "sd:queue", label: "Queue · chat bot<br/>4 in flight" }
  Q3@{ icon: "sd:queue", label: "Queue · CI vendor<br/>breaker open" }
  SEND["Senders<br/>10 s timeout, log result"]:::service
  LOG@{ icon: "sd:log", label: "Delivery log<br/>3 days, redeliver API" }
  EXT@{ icon: "sd:globe", label: "Receivers<br/>verify, dedupe, queue" }
  EV ==> FAN
  FAN ==> Q1
  FAN ==> Q2
  FAN -.-> Q3
  Q1 ==> SEND
  Q2 ==> SEND
  SEND ==> EXT
  SEND --> LOG
  class EV queue
  class Q1,Q2,Q3 queue
  class LOG store
  class EXT external
  • Per-destination queues and caps. A destination is a host, not a hook: thousands of repositories may point at one CI vendor, so cap in-flight deliveries per host.
  • Breakers per host. After a run of timeouts or 5xx responses, stop sending, keep queueing for a bounded time, probe occasionally, close on success.
  • A delivery has a shelf life, set per event type and recorded in the log when it expires. In 2018 an internal TTL dropped ~200,000 payloads until GitHub raised it: make the shelf life explicit, visible in the log and recoverable by redelivery.
  • Order is not promised; ramp the recovery. After a retry, two events for one pull request can arrive in either order, so receivers fetch current state when order matters. A host that comes back is drained at a ramped rate.
pythonhandle · 21 lines
# receiver/github_hook.py

"""
A receiver that does what GitHub's docs ask (illustrative): verify the signature,
answer within 10 seconds by queueing, and use X-GitHub-Delivery as an idempotency key.
"""

import hashlib
import hmac


def handle(headers: dict, body: bytes, secret: bytes, seen, queue) -> int:
    sig = headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return 401                                      # forged or tampered
    delivery = headers["X-GitHub-Delivery"]             # same GUID on redelivery
    if not seen.add_if_absent(delivery, ttl_s=7 * 86400):
        return 200                                      # already handled: acknowledge, do nothing
    queue.put({"event": headers["X-GitHub-Event"], "delivery": delivery, "body": body})
    return 202                                          # well inside 10 s; the rebuild happens later

Deep Dive 5: GitHub Actions: Queueing, Runners, Isolation and Cost (Week 9 Concepts: Tenant Isolation, Noisy Neighbor; Week 3: Backpressure; Week 10: Capacity Planning)#

You

"Actions runs arbitrary code from the internet for many tenants. The hard parts: admitting work faster than the fleet grows without collapsing, sharing the fleet fairly, and making sure a job can reach nothing it wasn't given, including what the previous job left behind."

This builds on Week 9, Day 1 · Tenant Isolation, Week 9, Day 2 · Noisy Neighbor Prevention, Week 3, Day 3 · Backpressure and Flow Control and Week 10, Day 4 · Capacity Planning.

Your turn · checkpoint 8not answered

A 256-job matrix from a stranger's fork

A first-time contributor opens a pull request against Nandini's repository from a fork. Its workflow file has been edited to a 256-job matrix, each job running a crypto miner for 6 hours. At the same moment, an enterprise with 10,000 engineers starts its nightly builds, and Nandini's 24 release jobs are waiting. Who runs first? What can the fork's jobs see? What stops the next job on the same machine from finding the miner, or the release signing key? And what does all this cost you?

Think it through, or jot it down. Then compare.

One strong answer

  • Admission: workflows from first-time contributors' forks can be held until a maintainer approves them. Even when they run, the fork's jobs get no repository secrets and a read-only token.
  • Fair share: every account has a concurrency cap (20 jobs at once on the Free plan); jobs beyond it wait in that account's own queue. The scheduler picks the next job across accounts, not from one global line, so the enterprise's 10,000 jobs can't push Nandini's 24 to the back.
  • Isolation: each hosted job runs on a new VM that's destroyed afterwards; nothing persists into the next job. Release secrets are scoped to the tag-triggered workflow (and can be gated by an environment with required reviewers).
  • Limits and cost: 6 hours per job, 256 jobs per matrix, mining pools blocked in the hosts file. At an assumed 5 minutes a job, ~430,000 VMs are busy, so every second of boot or wasted retry is multiplied by that.

What is published: Actions ran about 23 million jobs a day in early 2024; GitHub rebuilt its core backend aiming "to scale 10x over existing usage", and by December 2025 the new architecture handled 71 million a day, with individual enterprises able "to start 7x more jobs per minute"; Octoverse 2026 counts 124 million a day on average over the year to September 2026. GitHub's docs: "each GitHub-hosted runner is a new virtual machine" (single-CPU runners share a VM in containers); eligible Linux jobs may run on AWS or Azure; hosted runners "execute code within ephemeral and clean isolated virtual machines", while self-hosted runners "should almost never be used for public repositories". Limits include 6 hours per job, 256 jobs per matrix, 1,500 triggering events per 10 seconds per repository, and 20 (Free), 60 (Team) or 500 (Enterprise) concurrent standard jobs. Forks get no secrets and a read-only GITHUB_TOKEN. In 2022 GitHub described its run orchestrator as "a durable state machine" that, each time it wakes up, replays execution from the beginning to rebuild its state.

GitHub's availability reports show where it bends. On 2 February 2026, a policy wrongly applied in the compute provider "blocked access to critical VM metadata", so no VM could be created and hosted runners were down for hours in every region, while self-hosted runners elsewhere kept working. On 6 August 2026, a routine deployment briefly cut capacity; during recovery "runners were handed jobs that had already been revoked, then got stuck retrying them... which created a self-amplifying backlog". On 26 August 2026, a burst of events saturated a shared database and "there was no automatic circuit breaker to throttle inbound actions load". GitHub's summary of the 26 August incident: "our shared infrastructure services have not kept up with our month-over-month actions growth and peak load."

DiagramA hosted job's life
%% title: A hosted job's life
stateDiagram-v2
  [*] --> PENDING_APPROVAL: fork PR, first-time contributor
  [*] --> QUEUED: trigger admitted
  PENDING_APPROVAL --> QUEUED: maintainer approves
  PENDING_APPROVAL --> EXPIRED: 30 days
  QUEUED --> ASSIGNED: fair-share pick, lease epoch n
  ASSIGNED --> QUEUED: lease lapsed, epoch n+1
  ASSIGNED --> RUNNING: VM accepts, epoch matches
  RUNNING --> COMPLETED: steps done
  RUNNING --> FAILED: step failed or 6 h limit
  QUEUED --> CANCELLED: user or concurrency group
  COMPLETED --> [*]
  FAILED --> [*]
  CANCELLED --> [*]
  EXPIRED --> [*]
  class COMPLETED ok
  class FAILED bad
  class ASSIGNED queue
  • Admit, then queue per account. Triggers pass per-repository and per-account rate limits; admitted jobs wait in the account's queue, and its concurrency cap is its share of the fleet.
  • Schedule across accounts. Take the next job from the account furthest below its share (deficit round robin), per runner label. One enterprise's burst raises its own queue time, not everyone's.
  • Warm pools per label. Keep booted, never-used VMs sized from a 15-minute forecast per image and size, top up reactively, and destroy each VM after its job. The pool is the price of isolation; size it with Week 10's capacity planning.
  • Leases with epochs. An assignment carries an epoch the VM must present to start and report. A lapsed lease returns the job to the queue with a new epoch; a runner holding the old one is refused at once. That turns the August 2026 pattern into one fast rejection.
  • A breaker on intake. When the job database's latency or lag crosses a threshold, slow intake automatically and let queues absorb the burst; delayed jobs beat a saturated primary.
  • Cells and narrow tokens. Split the control plane into cells of accounts with their own databases and queues; give each job a token scoped to its repository that expires with the job.
pythonpick_next · 34 lines
# actions/scheduler.py

"""
Fair-share job assignment with leases (illustrative): the next job comes from the
account furthest below its share, and a lease carries an epoch so a runner holding
a revoked assignment is refused at once.
"""

import time

LEASE_S = 120


def pick_next(accounts: dict, label: str):
    ready = [a for a in accounts.values()
             if a.queue(label) and a.running < a.max_concurrent]
    if not ready:
        return None
    a = min(ready, key=lambda a: a.used_recently / a.weight)     # deficit-style fairness
    return a.queue(label).pop()


def assign(job, vm, store) -> dict:
    epoch = store.incr(f"job:{job.id}:epoch")
    store.put(f"lease:{job.id}", {"vm": vm.id, "epoch": epoch, "until": time.time() + LEASE_S})
    return {"job": job.id, "epoch": epoch, "token": mint_token(job.repo, job.permissions, ttl_s=job.max_s)}


def runner_report(store, job_id: str, epoch: int, vm_id: str, status: str) -> str:
    lease = store.get(f"lease:{job_id}")
    if lease is None or lease["epoch"] != epoch or lease["vm"] != vm_id:
        return "REVOKED"                                # stop now, take new work
    store.put(f"job:{job_id}:status", status)
    return "OK"
Quick check4 questions · +5 XP each

Pull requests, webhooks and Actions

  1. Q1/4 Why did GitHub split RepositoryPushJob into many jobs fed by one Kafka event?

    Answer & explanation

    Answer: B

    Fully processed pushes went from about 99.897% to at least 99.999%.

  2. Q2/4 A receiver takes 40 seconds to answer each webhook. What happens, and what's the fix?

    Answer & explanation

    Answer: B

    GitHub cuts the connection after 10 seconds and doesn't automatically redeliver.

  3. Q3/4 An enterprise starts 10,000 jobs while Nandini has 24 waiting. What keeps her jobs from waiting behind all of them?

    Answer & explanation

    Answer: B

    Fair share bounds what one tenant's burst does to everyone else.

  4. Q4/4 In August 2026, runners got stuck retrying jobs that had already been revoked. What design prevents that?

    Answer & explanation

    Answer: C

    Fencing with an epoch turns a stuck retry into a fast rejection.

You

"Code search breaks search engine habits: punctuation matters, no stemming, no stop words, regular expressions, and every result filtered by who may see it. But code is massively duplicated, and Git already names every file by its content."

This builds on Week 7, Day 1 · Search Fundamentals and Architecture, Week 7, Day 2 · Indexing Pipeline and Week 7, Day 5 · Search Operations and Scale.

Your turn · checkpoint 9not answered

Why not just grep?

At 11:20 someone searches all public code for parse_pin(. The index covers 115 TB of code. ripgrep scans about 0.6 GB a second per core. On a cluster of 32 machines with 64 cores each, how long does one exhaustive scan take, and how many queries a second can the cluster serve that way? What would you build instead, how would you shard it, and how do you make sure a query never returns code from a private repository the searcher can't see?

Think it through, or jot it down. Then compare.

One strong answer

  • Grep: 2,048 cores × 0.6 GB/s ≈ 1.2 TB/s, so 115 TB takes ~96 seconds: about 0.01 queries a second for the whole cluster.
  • Index instead: an n-gram inverted index (substrings of code → sorted lists of document ids). A query becomes intersections of a few posting lists, then a check of the candidate files.
  • Shard by content: use the Git blob id (the hash of the file's content) as the shard key. Identical files in a thousand forks are one document, stored once, and shards stay even because hashes are uniform.
  • Permissions: rewrite every query to add "public, or one of the repositories this user can read", and check permissions again on the results before returning them.

What is published: GitHub built Blackbird "from scratch, in Rust" because general text search gave a poor experience for code, indexed slowly and cost too much; its first Elasticsearch index of about 8 million repositories "took months". At the 2023 beta it covered "almost 45 million repositories, representing 115 TB of code and 15.5 billion documents". It is sharded by Git blob id, delta-encodes similar repositories so only unique blobs are crawled, and uses Kafka so that "each shard consumes a single Kafka partition", giving results "consistent on a commit-level basis". It indexes variable-length "sparse grams" rather than plain trigrams. Queries are rewritten with permission clauses, fanned out to every shard, re-sorted and permission-checked again. Per-shard p99 is on the order of 100 ms (an upper bound of about 640 queries a second per 64-core host); ingest runs at about 120,000 documents a second, re-indexing everything in about 18 hours; 115 TB became 28 TB of unique content and a 25 TB index. On 1 April 2026 code search was fully down for 2 hours 20 minutes; GitHub reset the index to an earlier point and re-indexed, since "the search index is a secondary index derived from Git repositories".

DiagramIndexing code: from a push to every shard
%% title: Indexing code: from a push to every shard
flowchart TD
  EV@{ icon: "tech:kafka", label: "Push events<br/>repo, new commit" }
  CR["Crawlers<br/>diff against delta parent, new blobs only"]:::service
  SYM["Symbol extraction<br/>definitions, references"]:::service
  DOC@{ icon: "tech:kafka", label: "Documents topic<br/>partitioned by blob id" }
  SH["Index shards<br/>one partition each, sparse grams"]:::core
  CMP@{ icon: "sd:database", label: "Compaction<br/>merge segments, rank order" }
  Q["Query service<br/>parse, add permissions, fan out"]:::hot
  EV ==> CR ==> SYM ==> DOC ==> SH ==> CMP
  Q ==>|"every shard"| SH
  class EV,DOC queue
  class CMP store
  • Content-addressed sharding means a fork adds only metadata (one more location for the same blob), not another document. The price: every query touches every shard, so tail latency comes from the slowest one; replicate shards and hedge slow requests.
  • Consistency by commit. Because each shard consumes its Kafka partition in order, a repository's results reflect one commit or the previous one, never half of each.
  • Permissions twice: in the query, so shards skip invisible documents, and on the results, so a stale permission cache can't leak a private file.
  • A secondary index must be cheap to rebuild: re-index from Git at a known rate and keep point-in-time rollbacks.
  • Retries need a budget, in any search system. On 12 August 2025 "GitHub search" (the report describes broken issue, pull request and project pages and filters, not code search) was degraded for nearly four hours after retry queues "eventually overwhelmed the load balancers".
pythongrams · 24 lines
# search/query.py

"""
One shard's part of a code search (illustrative): n-gram posting lists are
intersected, locations are filtered to what the searcher may see, and candidates
are verified against the content.
"""

def grams(s: str, n: int = 3) -> set:
    return {s[i:i + n] for i in range(len(s) - n + 1)}


def search_shard(shard, needle: str, visible_repos: set, limit: int = 100) -> list:
    postings = [shard.postings(g) for g in grams(needle)]          # sorted doc ids, best first
    candidates = set.intersection(*map(set, postings)) if postings else set()
    hits = []
    for doc in sorted(candidates):                                  # ids are assigned by rank
        locations = [loc for loc in shard.locations(doc)            # (repo, path, ref) for this blob
                     if loc.public or loc.repo_id in visible_repos]
        if locations and needle in shard.content(doc):              # drop trigram false positives
            hits.append((doc, locations))
            if len(hits) == limit:
                break
    return hits
Quick check3 questions · +5 XP each

Code search

  1. Q1/3 Grepping 115 TB on 2,048 cores at ~0.6 GB/s per core gives roughly how many queries a second?

    Answer & explanation

    Answer: C

    115 TB ÷ 1.2 TB/s ≈ 96 s per query.

  2. Q2/3 Why shard the code index by Git blob id rather than by repository?

    Answer & explanation

    Answer: B

    The cost is that every query must fan out to every shard.

  3. Q3/3 Code search was down and stale in April 2026, yet no data was lost. Why?

    Answer & explanation

    Answer: B

    A derived index is rebuildable as long as the source of truth is intact.

Phase 5 · Stage 5 of 7 · 5 min budget · 2 min read

Phase 5: Scaling and Edge Cases (5 minutes)#

Interviewer

"Walk me through Nandini's release day from the platform's side, then the cases that bite."

Nandini's Release Day#

A release in Bengaluru (the replication and merge queue follow GitHub's published rules; the timings are illustrative)
  • 10:40 · Merge queue
    • Her last pull request joins the queue; a temporary gh-readonly-queue/main/... branch with main plus the two pull requests ahead is built and tested, then merged
  • 11:02 · Tag push over SSH
    • The proxy checks her key and write permission; the pack goes to all five replicas; the tag reference commits when a majority agree with matching checksums
  • 11:03 · 24 jobs
    • The release run's jobs wait in her organization's queue and start on fresh VMs from warm pools; each gets a token scoped to her repository
  • 11:10 to 12:00 · The fan-out
    • Thousands of downstream pipelines fetch the tag; reads go to the nearest in-sync replicas, mostly answered from reachability bitmaps

Edge Cases#

Edge cases
Edge case 1: A force push races a fetch

Problem: A maintainer force-pushes main back two commits while CI is fetching it.

Solution: The reference update is one vote, so a fetch sees the old tip or the new one; unreachable objects stay until garbage collection, so the fetch completes. Branch protection can forbid force pushes to main.

Edge case 2: A leaked host key

Problem: The private key that proves "this really is GitHub" for SSH is exposed.

Solution: Rotate it at once, publish the new fingerprints, and accept a wave of "host identification has changed" warnings. GitHub did exactly this for its RSA SSH host key on 24 March 2023, after it was "briefly exposed in a public GitHub repository"; ECDSA and Ed25519 users, and HTTPS, were unaffected.

Edge case 3: A receiver down for four days

Problem: A team's webhook endpoint is down from Friday to Tuesday, longer than the 3-day redelivery window.

Solution: Webhooks are a notification, not the record. Receivers reconcile on recovery by reading current state from the REST API (open pull requests, latest commits, workflow runs) and redeliver what's still in the window.

Quick check2 questions · +5 XP each

Scaling and edge cases

  1. Q1/2 A webhook receiver was down for four days. How does it catch up?

    Answer & explanation

    Answer: B

    Webhooks notify; the API is the record. Treat them as hints and reconcile.

  2. Q2/2 A force push to main happens during a CI fetch. What can the fetch see?

    Answer & explanation

    Answer: B

    A reference update is atomic, and objects aren't deleted under a running reader.

Phase 6 · Stage 6 of 7 · 1 min

Phase 6: Monitoring and Operations#

You

"Four questions, all day: are pushes accepted, is every push fully processed, do CI jobs start on time for every customer, and are we a good citizen to receivers?"

Monitoring Dashboard#

Platform dashboard — 11:05 IST, a Thursday (illustrative)
Git
Metric Now
Pushes 1,420/s
Push acknowledged, p99 640 ms
Votes not unanimous (last hour) 3
Replicas below target count 0.002% of repos
Fetches 910,000/s
Events and webhooks
  • Pushes fully processed (24 h): 99.9993%; webhook success 97.1%, breakers open on 1,206 hosts
Actions
  • Jobs started: 1,610/s; queue time p50 / p95: 6 s / 41 s
  • Warm VMs idle: 6.8%; runner leases refused as stale: 12/min
  • Job database primary CPU: 58%; intake throttle: off
Search
  • Query p99: 420 ms; index lag p95: 38 s

SLOs for the Platform#

SLOs for Git, events, Actions and search
SLO 1: Pushes land
  • Target: 99.95% of pushes acknowledged within 1 s; zero acknowledged pushes lost; zero reference regressions seen by readers
SLO 2: Every push is fully processed
  • Target: 99.999% of pushes have every follow-up task done (GitHub's 2024 pipeline measured at least this); p99 push-to-webhook-sent under 10 s
SLO 3: CI starts on time, for everyone
  • Target: p95 queue time under 60 s for hosted jobs within an account's concurrency limit, measured per account, not just in aggregate
SLO 4: Search answers and stays fresh
  • Target: 99.9% of code searches under 1 s; 95% of pushes searchable within 2 minutes
Quick check2 questions · +5 XP each

Reading the dashboard

  1. Q1/2 Aggregate Actions queue time is fine, but one enterprise's p95 is 20 minutes and it's within its concurrency limit. What does that suggest?

    Answer & explanation

    Answer: B

    Fairness bugs hide in averages. Measure per account and per runner label.

  2. Q2/2 "Runner leases refused as stale" jumps from 12 a minute to 40,000. What's happening?

    Answer & explanation

    Answer: B

    Without the epoch check, this is the August 2026 self-amplifying backlog.

Final challenge · Stage 7 of 7 · 2 min

Final Challenge: Two Thousand Agents on One Monorepo, During a Migration#

The interviewer leans back. "One more."

Interviewer

"A customer turns on a fleet of 2,000 coding agents against their monorepo. Each agent pushes to its own branch every few minutes, opens a pull request, and its CI fetches the repository each time. All of them merge through one merge queue into main. Meanwhile, you're halfway through moving this repository's replicas to a new region 25 ms away. Tell me what breaks first and what you change."

The agent fleet's brief
  • 2,000 agents, each pushing about every 3 minutes → ~11 pushes a second to one repository (assumption)
  • Each push triggers CI that fetches the repository: ~11 full fetches a second, plus test merges for every open pull request
  • ~300 merges an hour into main through the merge queue (assumption)
  • Replicas: three in the old region, two in the new one, 25 ms apart
  • The monorepo has hundreds of thousands of references and receives maintenance every 50 pushes
Your turn · checkpoint 10not answered

What do you change for the agent fleet?

Decide what happens to the write path, the read path, the merge queue, maintenance and CI.

Think it through, or jot it down. Then compare.

One strong answer

  • Writes: with a repository-wide lock at ~0.3 s per cross-region update, 11 pushes a second need 3.3 s of lock time every second. Per-reference serialization lets 2,000 branches move in parallel; only main stays serialized. Coalesce test-merge updates and put pushes first.
  • Reads: serve repeated CI fetches from read replicas or caches near the runners, with blobless or shallow clones; grow reads without adding voters.
  • The merge queue: main is the hot key. Batch several pull requests per merge group, raise build concurrency, eject failures fast.
  • Maintenance: 50 pushes arrive in under 5 seconds, so it must be geometric, off the serving path, and retried in quiet hours.
  • The migration: keep a voting majority in one region until per-reference quorums are on; add far replicas as non-voting first.
  • CI: the account's cap, queue and cell keep 2,000 agents from crowding out others; size its warm pools from its own forecast.
Quick check2 questions · +5 XP each

Final challenge · Quest check

  1. Q1/2 2,000 agents push to their own branches about 11 times a second. What change helps most on the write path?

    Answer & explanation

    Answer: B

    Git's guarantee is per reference; only writes to the same branch need ordering.

  2. Q2/2 Why add the far-region replicas as non-voting first?

    Answer & explanation

    Answer: B

    GitHub Enterprise's geo-replicas were already non-voting in 2017, so that pushes keep working if they become unreachable.

Wrap-up · 7 min

Interview Conclusion#

Transcript
Interviewer

"Why not give each repository a primary fileserver and replicate to the others asynchronously? Databases do it."

You

"Because losing the primary means an election, and a bad one splits history: GitHub's MySQL clusters showed that in 2018, when primaries moved across the country during a 43-second partition. Spokes makes every write its own majority vote, so there's nothing to fail over; without a majority it refuses the push, which is the right failure for source code."

Interviewer

"And why not retry webhooks until they succeed?"

You

"Because a push webhook delivered an hour late is often worse than none, and a recovering receiver can't take a day's backlog at once. Bounded attempts with a shelf life, per-host breakers, a 3-day delivery log with redelivery, and a delivery id that makes repeats harmless put the receiver in control of catching up."

Interviewer

"The single most important decision?"

You

"Splitting the push into one small thing that needs agreement, the reference update, and everything else, which only needs an event. That's what lets the storage vote stay fast, lets a webhook outage leave CI alone, and lets 124 million jobs a day run without touching the write path."

Summary: Concepts Applied from 10-Week Course#

Concepts from the 10-week course in a code hosting platform
  • Week 1: Data at scale
    • Five replicas, majority writes, nearest in-sync reads; main as a hot key; rate limits at the Git proxies
  • Week 2: Failure-first design
    • Webhooks with a 10-second timeout, delivery ids and per-host breakers; timeouts sized per Git operation
  • Week 3: Messaging and async processing
    • One Kafka event per push, isolated consumers, dead letters; back-pressure on CI intake
  • Week 4: Caching
    • Read replicas as caches; reachability bitmaps and cached packs for hot commits
  • Week 5: Consistency and coordination
    • Every write a majority vote, per-reference serialization; the 2018 cross-region failover as the counter-example
  • Week 7: Search
    • An n-gram index sharded by blob id, ordered ingestion, a rebuildable secondary index
  • Week 9: Multi-tenancy and security
    • One VM per CI job, scoped tokens, no secrets for forks, fair share, cells
  • Week 10: Production readiness
    • Warm pools from forecasts; per-account SLOs

Why GitHub Matters#

Why this is a different kind of hard problem
Most of the data is immutable
  • Git objects are named by their content, so replicas, caches, forks and the search index share them freely; only references need agreement, and GitHub's 2026 design narrows that to one reference at a time
One write fans out into a city of work
  • A push becomes pull request updates, webhooks, CI jobs and index updates. Keeping each one isolated, with its own retries and deadlines, is what moved fully processed pushes from about 99.897% to at least 99.999%
The workload changed under the system
  • Write volume multiplied in a year as agents arrived (pushes 4.9×, Actions to 124 million jobs a day) while GitHub moved to Azure; its 2026 availability reports show the strain in the open: capacity, retries and shared databases

"Agree on the reference, broadcast the event, and run strangers' code on machines you throw away."

Self-Assessment Checklist#

After studying this case study, you should be able to:

Storage and Git traffic:

Storage and Git traffic0/2 ready
  • Explain why Spokes writes to a majority of replicas and treats every write as a vote, and contrast it with single-primary failover
  • Route reads to in-sync replicas, drain a server without breaking long clones, and choose clone filters for CI

Events, pull requests and webhooks:

Events, pull requests and webhooks0/2 ready
  • Split push processing into isolated consumers with their own retry rules and dead letters
  • Design webhook delivery with per-host isolation, a timeout, a shelf life, signatures and idempotent receivers

Actions and search:

Actions and search0/2 ready
  • Schedule CI jobs fairly across accounts with concurrency caps, warm pools and leases with epochs
  • Explain an n-gram code index sharded by blob id, with permissions applied twice

Sources#

GitHub Engineering: Git storage and traffic:

GitHub Engineering: events, pull requests, metadata:

GitHub Actions:

Webhooks and merge queue:

Code search:

Octoverse and availability reports:

Design note: the architecture, code, thresholds, dashboards and every number labelled as an assumption or estimate are an illustrative design for the problem, not a description of GitHub's internal systems beyond what GitHub has published. The peak multipliers, average repository size, average job length, VM boot time, webhook event and delivery counts, the scheduler, the pack cache, the webhook queueing design and the final challenge's figures are assumptions. Nandini, her library and her co-maintainer are fictional.

Further Reading#

Related systems to study:

  • Book

    AWS S3 (Bonus Problem 8 in this series): durability by copies and erasure coding, for objects that never change

  • Read

    Google Search (Bonus Problem 5): an inverted index over the web, sharded and served in milliseconds

  • Read

    Razorpay (Bonus Problem 18): webhooks to merchants, signatures and idempotent receivers from the payments side

  • Read

    ChatGPT (Bonus Problem 14): another fleet of expensive machines shared fairly between tenants

War story:

Foundational concepts (this course):

  • Read

    Week 1, Day 2 — Replication Trade-offs

  • Read

    Week 1, Day 4 — Hot Keys and Skew

  • Read

    Week 2, Day 2 — Idempotency in Practice

  • Read

    Week 2, Day 3 — Circuit Breakers

  • Read

    Week 2, Day 4 — Webhook Delivery System

  • Read

    Week 3, Day 1 — Queue vs Stream

  • Read

    Week 3, Day 4 — Dead Letters and Poison Pills

  • Read

    Week 4, Day 5 — Multi-Tier Caching

  • Read

    Week 5, Day 5 — Leader Election

  • Read

    Week 7, Day 2 — Indexing Pipeline

  • Read

    Week 9, Day 1 — Tenant Isolation

  • Read

    Week 9, Day 2 — Noisy Neighbor Prevention

  • Read

    Week 10, Day 4 — Capacity Planning

"Agree on the reference, broadcast the event, and run strangers' code on machines you throw away."

this lesson in shorts · 4

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Side quest 29 · GitHub
Clear the questFinish GitHub.Stages clear as you read them. Marking complete unlocks after ~8 more min of reading.
  • 0/7stages cleared
design sheet · 7 diagrams · pdfRevise the whole design in ten minutesThe numbers, architecture, sequence, class and state diagrams, decisions and failure modes of this quest on a few printable pages.open the sheet →

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…