At a glance#
| Number | Value | What it drives |
|---|---|---|
| Repositories | ~1 billion · five replicas by default | ~100 PB on fileservers at an assumed 20 MB each |
| Pushes | 3.35B a month · ~1,300 / s | design for ~5,000 / s; each push is a small distributed transaction |
| Reads per write | 100 to 1,000 | 130,000 to 1.3M Git reads / s; reads must scale without adding voters |
| Spokes operations | 100B+ a day ≈ 1.2M / s | routing, voting and repair are one busy service |
| Work after a push | 60+ pieces of logic · 20 services | one Kafka event, isolated consumers, per-task retries |
| Actions jobs | 124M a day · 1,440 / s (2026 average) | ~430,000 VMs busy at an assumed 5 minutes a job |
| Webhooks | 2XX within 10 s · 3-day redelivery | per-host queues and breakers; delivery id as idempotency key |
| Code search | 15.5B docs · 25 TB index · ~100 ms p99 per shard | n-gram index sharded by blob id; every query hits every shard |
Architecture#
DiagramVote on the write, fan out the event, run CI on disposable machines
%% title: Vote on the write, fan out the event, run CI on disposable machines
flowchart TD
DEV@{ icon: "sd:laptop", label: "Developers and agents<br/>git over SSH or HTTPS" } ==> LB@{ icon: "sd:load-balancer", label: "GLB, layer 4<br/>stateless directors" }
LB ==> GITP["Git proxies<br/>auth · rate limits"]:::service
LB --> APP["Rails monolith<br/>web · API · PRs"]:::service
APP --> MY@{ icon: "tech:mysql", label: "MySQL and Vitess<br/>PRs · issues · users" }
GITP ==> SPK["Spokes<br/>route · vote · repair"]:::hot
SPK ==> FS@{ icon: "sd:servers", label: "Fileservers<br/>five replicas" }
SPK -.-> BLOB@{ icon: "sd:object-storage", label: "Azure Blob Storage<br/>virtual replica" }
SPK ==>|"ref updated"| K@{ icon: "tech:kafka", label: "Kafka<br/>one event per push" }
K ==> JOBS@{ icon: "sd:worker", label: "Push jobs<br/>per-task retries" }
K --> SRCH@{ icon: "sd:search", label: "Code search<br/>sharded by blob id" }
JOBS ==> HOOK@{ icon: "sd:webhook", label: "Webhook delivery<br/>per-host queues" }
JOBS ==> ACT["Actions control plane<br/>fair share · leases"]:::core
ACT ==> RUN@{ icon: "sd:server", label: "Runner VMs<br/>one job each" }
class DEV client
class LB edge
class MY,FS,BLOB store
class K queue
class JOBS,SRCH,HOOK,RUN service- Git data and metadata are separate: repositories on fileservers replicated by Spokes; pull requests, issues and users in MySQL.
- Only the reference update needs agreement: everything after it reads one event and can lag, retry or fail on its own.
- CI is isolated per job, not per customer: a fresh VM, a scoped token, then the VM is destroyed.
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["Lock on a majority<br/>per reference, rolling out 2026"]:::core
CHECK{"Old value matches?"}
APPLY["Apply tentatively<br/>XOR checksum update"]:::core
VOTE{"Majority with<br/>equal checksums?"}
COMMIT(["Commit · acknowledge"]):::ok
ROLL(["Roll back · reject"]):::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 serviceKey flows#
DiagramNandini pushes the tag v4.0.0
%% title: Nandini pushes the tag v4.0.0 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, key and write permission checked P->>S: Which replicas hold the 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: Applied, checksums match on a majority S->>R: Commit P-->>N: new tag v4.0.0 S->>K: Push event, repo, ref, old and new SHA K->>J: PR sync, webhooks, workflow trigger, indexing Note over J: each consumer has its own offset and retry policy
DiagramWebhooks to a fast receiver and a dead one
%% title: Webhooks to a fast receiver and a dead one
sequenceDiagram
autonumber
participant F as Fan-out
participant Q as Per-host queues
participant S as Senders
participant B as Chat bot
participant V as CI vendor
F->>Q: Signed delivery, delivery id, per subscribed host
Q->>S: Chat bot queue, within its cap
S->>B: POST, 10 s timeout
B-->>S: 200 in 50 ms
Q->>S: CI vendor queue
S->>V: POST, 10 s timeout
alt vendor keeps timing out
S-->>Q: Breaker opens · stop sending, probe now and then
Note over Q: queued only until the delivery's shelf life · then logged as failed
else vendor recovers
S->>V: Drain at a ramped rate
V-->>S: 2XX · duplicates skipped by delivery id
endDiagramAn Actions job with a lease and an epoch
%% title: An Actions job with a lease and an epoch
sequenceDiagram
autonumber
participant T as Trigger
participant C as Control plane
participant V1 as Runner VM 1
participant V2 as Runner VM 2
T->>C: Workflow admitted, jobs into the account's queue
C->>V1: Assign job, epoch 1, scoped token
Note over V1: VM 1 stalls, lease lapses
C->>V2: Reassign job, epoch 2
V2-->>C: Running, epoch 2
alt VM 1 comes back
V1->>C: Report with epoch 1
C-->>V1: REVOKED · take new work
end
V2-->>C: Completed · VM destroyedData model#
DiagramCore entities
%% title: Core entities
classDiagram
class Repository {
+repoId : int
+replicas : five fileservers
+checksum : XOR of ref hashes
}
class Reference {
+name : branch or tag
+sha : commit id
+lock : per reference
}
class PushEvent {
+eventId : string
+oldSha : string
+newSha : string
}
class PullRequest {
+headRef : string
+testMergeRef : hidden reference
+mergeable : derived
}
class WebhookDelivery {
+deliveryId : GUID, same on redelivery
+host : destination
+status : delivered or failed
}
class WorkflowJob {
+account : string
+label : runner image and size
+epoch : int
+state : QUEUED to COMPLETED
}
Repository "1" --> "many" Reference : holds
Reference "1" --> "many" PushEvent : each update
PushEvent "1" --> "many" WebhookDelivery : per subscribed host
PushEvent "1" --> "many" WorkflowJob : triggers
Repository "1" --> "many" PullRequest : open- Repositories are partitioned by repository across fileservers, five replicas each in different racks; search documents are partitioned by Git blob id, so identical files across forks are one document.
- Pull requests, users and permissions live in MySQL, not in Git; a pull request's mergeability is a test merge stored as a hidden reference.
Lifecycle#
DiagramA hosted Actions job
%% title: A hosted Actions job 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 · 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
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Repository writes | Majority vote per write, checksums | One primary per repository | no election to get wrong; without a majority it refuses, never splits |
| Lock scope | Per reference (rolling out, 2026) | Repository-wide lock | 25 ms cross-region round trips capped busy repositories |
| Read scaling | Cache-only read replicas over object storage | More voting replicas | each voter added work to every write |
| After the push | One Kafka event, isolated consumers | One big push job | per-task retries; ~99.897% to 99.999% fully processed |
| Webhooks | Per-host queues, breakers, shelf life, 3-day redelivery | Retry forever | late events are useless; backlogs knock receivers over |
| CI isolation | Fresh VM per job, scoped token | Reused runners | strangers' code must not persist into the next job |
| CI fairness | Per-account queues and caps, leases with epochs | One global queue | one burst can't starve others; revoked jobs fail fast |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| A replica slow or far away | slower pushes on busy repos | per-reference locks; coalesce test merges; user pushes first |
| HTTPS Git proxy exhausted | HTTPS push and pull fail | separate SSH and HTTPS fleets; switch remotes |
| Webhook host down | red deliveries in the log | per-host breaker; shelf life; redeliver; reconcile via API |
| Actions database saturated | runs stuck queued | automatic intake breaker; queues absorb the burst |
| Runners hold revoked jobs | backlog that feeds itself | lease epochs; refuse stale runners at once |
| Search index corrupted | stale or failed code search | rebuild from Git; point-in-time rollback |
Say it in the interview#
- "Only the reference needs agreement": Git objects are immutable and named by content; replicas vote on reference updates, everything else is an event.
- A vote per write, not a primary: a majority with matching checksums commits; without one Spokes refuses, unlike the 2018 cross-region MySQL failover.
- Scope the lock to the reference: pushes to different branches stop waiting for each other;
mainstays the hot key, so use a merge queue. - Read replicas without votes: object storage holds the durable copy, so read capacity grows without slowing writes.
- One event, many consumers: each follow-up has its own retry rule; once-only tasks are keyed by the event id; failures land in dead letters.
- Webhooks belong to the receiver: 10-second timeout, per-host isolation, a delivery id for duplicates, a 3-day log to catch up.
- Throw the machine away: a fresh VM per CI job, fair share per account, and lease epochs so a revoked job fails fast.