GITLAB.COM 2016–2026  / field guide
Practitioner field guide · 20 September 2026

When the database cannot be split

Ten years of GitLab.com read out of its own public incident tracker, design documents and rejected merge requests. One company, two components that resisted horizontal scaling, and every architectural move since 2016 explainable as a response to one of them.

30 primary documents 1 production system 11 incident records Evidence through September 2026 Read: 35 min
01

The territory

A product grows at a fixed multiple every year. Underneath it sit two components that cannot be split without rewriting the application. What do you do for the next ten years?

That is the problem this guide is about, and GitLab is an unusually good place to watch it play out, because GitLab runs its own product in public. The incident tracker for GitLab.com is a public issue board whose first entries date from April 2016. The architecture design documents, including the ones marked rejected, are markdown files in a public repository. The service topology is a directory of configuration files anyone can list. A solution architect can therefore do something here that is normally impossible: follow a single company's stateful tier across a decade and check every claim against the artefact the engineers wrote at the time, rather than the retrospective they wrote afterwards.

The two components are a PostgreSQL cluster and a filesystem holding Git repositories. Neither could be sharded without changing the application above it, and GitLab's own capacity process says so in as many words: resources are classified by whether they scale horizontally, and non-horizontally scalable resources (such as CPU on the primary PostgreSQL instance, for example) require much longer-term strategies for remediation and are therefore considered higher priorities in the capacity planning process.32 Everything in the decade that follows is a long-term strategy for one of those two resources.

The finding worth your time

The most consequential number in GitLab's decade is not a latency or a request rate. It is 100 GB, a target for the on-disk size of a single physical table, enforced by static analysis that refuses new columns on tables above it. GitLab arrived at that rule after discovering that the size of one table had become the limit on how often the company could deploy: building a single index on ci_builds took between 1.5 and 6 hours, and this process blocks deployments as migrations are being run synchronously.13 The database stopped being a latency problem and became a release-engineering problem, and that is the transition most capacity models never predict.

2,975 GB
Total on-disk size of the ci_builds table, 22.7% of the entire database
1.5–6 h
To build one index on that table, blocking deployment while it ran
12 M
Sidekiq jobs backlogged in the May 2026 incident, caused by an earlier incident's mitigation
117
Service definitions in the production metrics catalogue, of which 14 are Redis services

Figure 1 · The landscape: one pressure, four families of response

Growth the architecture
did not choose

PostgreSQL primary
not horizontally scalable

Git repository storage
bound to one node

Shrink what is in it
size targets, partitioning,
CDC to ClickHouse

Split it by domain
main / ci / sec

Split it by tenant
Organizations on Cells

Replicate it
Praefect, then Raft
both abandoned

Move the truth elsewhere
object storage, proposed 2026

Growth the architecture
did not choose

PostgreSQL primary
not horizontally scalable

Git repository storage
bound to one node

Shrink what is in it
size targets, partitioning,
CDC to ClickHouse

Split it by domain
main / ci / sec

Split it by tenant
Organizations on Cells

Replicate it
Praefect, then Raft
both abandoned

Move the truth elsewhere
object storage, proposed 2026

Every major programme of the decade reduces to taking load off one of two components that could not be split. Reconstructed from the table size target, the Cells design document, the Scaling Git proposal and the Siphon design document.
Diagram source

What this guide covers. GitLab.com's server-side state and topology between 2016 and September 2026: the PostgreSQL estate, Git storage, the key-value tier, background jobs, search, the routing plane and the incidents that shaped them. What it does not cover. The product itself, the Duo model stack beyond where it appears in incidents, security and compliance architecture, the self-managed and Dedicated packaging paths, and the money. No infrastructure spend figure appears anywhere in this guide because none was found in a source that could be opened. GitLab's cost-management handbook page describes a process and links to dashboards that are not public.

02

How it is actually built

Reconstructed from the service catalogue, the controlled production architecture document and the Cells design, all of which are public files rather than diagrams drawn for a conference.

GitLab.com runs in Google Cloud's us-east1 region on four Kubernetes clusters, and the interesting sentence in the controlled architecture document is the exception clause: Most of GitLab.com is deployed on Kubernetes using GitLab cloud native helm chart. There are a few exceptions for this which are mainly the datastore services like PostgresSQL, Gitaly, Redis, Elasticsearch.33 Ten years of platform modernisation moved the stateless tier into containers and left the stateful tier exactly where it was. That split is the single most reliable pattern in this corpus, and it is worth noticing before anything else: the components that took a decade of architectural attention are precisely the ones that never became cattle.

The topology can be counted rather than estimated. The metrics catalogue that drives GitLab.com's monitoring holds 117 service definitions.27 Fourteen of them have names beginning redis, covering cache, sessions, shared state, rate limiting, feature flags, the Action Cable pub/sub path, the container registry, a repository cache and the Sidekiq queues; five more are Memorystore instances including a quarantine instance for misbehaving job classes. Four are Patroni-managed PostgreSQL clusters (main, ci, sec, registry), each with its own pgbouncer fleet. The single Redis and single PostgreSQL of 2016 have become nineteen key-value services and four database clusters, and each split was paid for separately.

Figure 2 · GitLab.com as of September 2026

Satellites

Stateful tier, not on Kubernetes

4 GKE clusters, stateless

Cell services

logical replication

Cloudflare edge
HTTP Router Worker

Topology
Service

Cloud
Spanner

web / api / git

Sidekiq
sharded by queue

Patroni x4
main, ci, sec,
registry

Redis x14
Memorystore x5

Gitaly nodes
repo per node

Zoekt

Elasticsearch

ClickHouse

Object
storage

Satellites

Stateful tier, not on Kubernetes

4 GKE clusters, stateless

Cell services

logical replication

Cloudflare edge
HTTP Router Worker

Topology
Service

Cloud
Spanner

web / api / git

Sidekiq
sharded by queue

Patroni x4
main, ci, sec,
registry

Redis x14
Memorystore x5

Gitaly nodes
repo per node

Zoekt

Elasticsearch

ClickHouse

Object
storage

The stateful tier in the middle row is the part that never moved into Kubernetes, and the Patroni and Gitaly boxes inside it are the two components the rest of this diagram exists to relieve. Component list from the metrics catalogue and the production architecture document; the cell services plane from the Cells design document.
Diagram source

The routing plane, bolted on in front

Requests are classified and routed to a cell by a Cloudflare Worker before they reach any GitLab code, and the authoritative map of which organisation lives on which cell is a Topology Service backed by Cloud Spanner. Both are new surfaces, in a different cloud from the application, introduced because routing had to exist somewhere that no single cell owns.

Source: Cells ADR 001, service catalogue

The job tier, sharded in the application

Sidekiq jobs are routed to a specific Redis by application-level rules rather than by Redis Cluster, because the key distribution defeats the cluster's hashing: Redis Cluster is not suitable for Sidekiq since there are only a small subset of hot keys. The recommended shape is one queue per shard, with a Kubernetes deployment sized per shard.

Source: Sidekiq sharding runbook

The analytics path, moved off the primary

Siphon reads the PostgreSQL logical replication stream, serialises change events onto a queue and lets consumers load them into ClickHouse or Snowflake. Its stated goal is zero or minimal impact on the availability of the PostgreSQL cluster, which is the same instinct as the table size target expressed in a different layer: take work off the component that cannot be split.

Source: Siphon design document, November 2024

Where the reconstruction is thinner: node counts, instance sizes and per-service capacity are not public. The metrics catalogue names every service and its service-level indicators but not its fleet size, and the forecasting outputs that would show saturation headroom are classified confidential under GitLab's own data standard.32 An architect can therefore learn the shape of this system in detail and cannot learn what it costs to run, which is an unusual combination and worth keeping in mind when this corpus is used as a benchmark.

03

The decisions that matter

Chosen, rejected, the stated reason, and the condition under which the rejected option becomes the right one.

GitLab keeps its rejected designs. The Cells directory contains a folder named rejected holding two complete router proposals, and one of them explains the practice in its own header: This documentation will be kept even if we decide not to implement this so that we can document the reasons for not choosing this approach.21 That habit is why this section can be written at all, and it is the cheapest thing in this guide for another organisation to copy.

Decision: how do you stop a table from getting bigger?

Chosen
  • A numeric target, 100 GB per physical table including indexes, with action starting at 10 GB
  • Enforced mechanically: ADR-002 limits new columns on tables above 100 GB, ADR-003 limits new indexes above 50 GB, and the static analysis tools refuse the change
Rejected
  • Case-by-case database review, which had been the practice
  • The reason it lost is measurable: index creation had reached 6 hours and was blocking deploys, and vacuum on the largest tables ran roughly once a day
Flips when
  • Your migration tooling can add a column or build an index online at your largest table's size without blocking release. The rule exists because the tooling could not.
  • Also flips if the table is append-only with a clean time dimension: partitioning gets you the same result without the prohibition.

Decision: how do you split one database into several?

Chosen
  • Decomposition by domain first: main, ci and sec run as separate clusters on GitLab.com
  • Isolation enforced in the application through a gitlab_schema classification applied to every table
Rejected
  • PostgreSQL schemas as the isolation mechanism: we cannot use PostgreSQL schema due to complex migration procedures
  • Sharding first. The 2021 Database Sharding blueprint was opened as a draft merge request and closed without ever being merged.
Flips when
  • Your domains are not separable but your tenants are. GitLab reached that point anyway: decomposition bought headroom, and the tenant split it postponed became the Cells programme a year later.

Decision: where does tenant routing live?

Chosen
  • A Cloudflare Worker in front of everything, with static rules and HTTP caching, deliberately kept small: the ADR estimates up to 1000 lines of code
  • Stated consequence accepted in writing: more vendor locking with Cloudflare, but we are already heavily dependent on them
Rejected
  • Istio, after a proof of concept
  • A stateless router that buffers requests and replays them when a cell bounces them back, rejected because it means a request can be processed more than once
  • A router that learns routes from pre-flight requests, rejected because it forces all routable information into the URI
Flips when
  • Self-managed parity matters. The ADR states plainly that Cloudflare Workers meet every requirement apart from the self-managed, which is a low priority requirement. If that requirement were high priority, this decision inverts.

Figure 3 · The decision tree GitLab walked, four times

yes

no

yes

no

rarely

often

yes

no

Component is saturating
and cannot be sharded

Can you remove
work from it?

Shrink it: retention,
partitioning, move analytics
to a CDC stream

Do clean domain
boundaries exist?

Decompose by domain,
enforce in the application

Do tenants share
data across the boundary?

Partition by tenant,
route in front of the app

Is the state
replicable?

Replicate and distribute reads

Move the source of truth
to shared storage,
make nodes stateless

yes

no

yes

no

rarely

often

yes

no

Component is saturating
and cannot be sharded

Can you remove
work from it?

Shrink it: retention,
partitioning, move analytics
to a CDC stream

Do clean domain
boundaries exist?

Decompose by domain,
enforce in the application

Do tenants share
data across the boundary?

Partition by tenant,
route in front of the app

Is the state
replicable?

Replicate and distribute reads

Move the source of truth
to shared storage,
make nodes stateless

Each terminal is an action, and each was taken by GitLab at least once between 2016 and 2026. Derived from the design documents cited throughout this section.
Diagram source

The fourth path on that tree is where GitLab's record is most instructive, because GitLab took the replication branch twice and abandoned it twice. Gitaly exists because Git on network storage was unusable: the original design document records a P99 of over 30 seconds to open a repository object against 15 milliseconds of CPU, and names the filesystem as the culprit.23 Moving to bare metal was considered and refused on the grounds that customers run in the cloud. Praefect then added replication and read distribution on top of Gitaly. A transaction manager, write-ahead log and Raft service came after that. In 2026 both are gone. The Scaling Git proposal states the verdict on the first: while we tried to address this issue with read distribution via Praefect, that effort has basically failed to yield a horizontally scalable cluster due to various issues.26 The transaction management design document now carries the status rejected and an unusually candid note: the design was largely built and is now being unwound, rather than turned down at review.24 The removal epic sizes the unwinding at a merge request of 365 files, too large to review as one MR.25

Figure 4 · Three attempts to scale Git storage, ten years apart

shipped

shipped

basically failed

built, then removed

Git on network storage
P99 over 30s to open a repo

Gitaly, 2016
RPC layer, local disk

Praefect, 2019
replicate and
distribute reads

Raft + WAL, 2023
transactional cluster

Object storage + MVCC
proposed 2026

shipped

shipped

basically failed

built, then removed

Git on network storage
P99 over 30s to open a repo

Gitaly, 2016
RPC layer, local disk

Praefect, 2019
replicate and
distribute reads

Raft + WAL, 2023
transactional cluster

Object storage + MVCC
proposed 2026

Two of the three were built and then removed. Only the third changes where the authoritative copy lives, which is the variable the first two left alone. From the Gitaly design document, the transaction management document and the Scaling Git proposal.
Diagram source
DecisionChosenRejectedBecauseEvidence
Queue pending CI buildsA separate, purpose-built tableRedis-backed queuingQueue building scanned the largest table in the estate; the team evaluated both and shipped the table in October 2021CI/CD Scaling, 2021
Shard the job queuesApplication-level routing to one Redis per queueRedis ClusterThere are only a small subset of hot keys, so cluster hashing does not spread the loadSidekiq sharding runbook
Code searchZoekt alongside ElasticsearchExtending Elasticsearch to cover codeCode search needs exact and regex matching; Elasticsearch stays for issues, merge requests and commentsZoekt design document, 2022
Analytics loadChange data capture off the replication stream into ClickHouseQuerying the OLTP replicasGoal stated as zero or minimal impact on PostgreSQL availabilitySiphon, 2024
Cell granularityOrganizations as the logical boundary, Cells as the physical oneSubdomains per tenantCookie leakage across subdomains, integration breakage, and naming collisions with higher impactCells design document
Disaster recovery for cellsBackup and restoreGeo replication (ADR 006, superseded by ADR 024)The decision log records both, in order, with the later one replacing the earlierCells decision log

One more decision deserves separate mention because it is about process rather than technology. The Cells programme is on its fourth design. The current document opens by saying so: Protocells is the current iteration of the Cells architecture, replacing the earlier Cells 1.0, Cells 1.5, and Cells 2.0 iterations with a new focus on permanently reducing load on the legacy cell's database.18 The narrowing is the interesting part. Cells 2.0 aimed at a public, open-source contribution model across cells; Protocells aims at one measurable outcome, taking load off one database, and explicitly defers cross-organisation contribution, users spanning cells and migration of public organisations to future scope. Four years of design iteration produced a smaller goal, not a bigger one, and the programme became deliverable at the point where it stopped trying to be complete.

04

What broke in production

Eleven incident records spanning 2016 to 2026, grouped by failure class rather than by year. Three of the four classes recur across the whole decade.

Read in bulk, GitLab's incident corpus sorts into four classes. The state layer failing quietly and being noticed by a human hours later. Maintenance work on a large table becoming an incident in its own right. Two control planes disagreeing about the same object. And the mitigation for one incident causing the next. The first and the last are the ones that span the entire decade unchanged.

Postmortem

Two primaries, five hours, nobody paged

AssumptionAutomated failover means the cluster state is known.
What happenedPacemaker failed over from DB4 to DB5 at 01:46 UTC. Both then accepted client connections, 402 on one and 231 on the other. The team chose DB5 as the victim.
Blast radiusSix hours of divergence, and data written to the losing node was lost. Detected by an engineer looking at a dashboard at roughly 06:40 UTC.
FixAlerting on failover; runbook corrections; the team wrote that the checks have to be executed manually, leaving space to interpretation.
Design ruleAn automated failover without an alert on the failover event is worse than a manual one, because it removes the human from the only step that was reliably observed.
Postmortem

The backup job that did not survive the cloud migration

AssumptionA corrective action from a previous data-loss incident stays implemented.
What happenedThree days after GitLab.com moved to Google Cloud, automated base backups were not running: the cron entries were simply absent in the new environment. The only backup was one an engineer had taken by hand.
Blast radiusNo user impact, and no detection either: I only noticed this because I looked into the automated restore host by chance. The automated restore had stopped because the backup was too old, and that condition did not alert.
FixAlerting on top of the automated backup restore, filed as a separate issue.
Design ruleA corrective action without its own alert has a half-life, and the environment migration is when it expires. Migrate the detection before the mechanism.
Postmortem

A CREATE INDEX that came within 12 hours of stopping the database

AssumptionIndex maintenance is background work with bounded cost.
What happenedA long-running index build blocked vacuum. Dead tuples accumulated and the on-call engineer found a CREATE statement that had been running for five and a half hours, with transaction ID wraparound approaching.
Blast radiusRecorded as a near miss: we came within 12 hours of a full database shutdown due to a long-running migration. A related incident three weeks later saw index creation saturate the network on the PostgreSQL leader through WAL volume.
FixStructurally, the 100 GB table size target and the static-analysis rules that followed it one month later.
Design rulePast a certain table size, maintenance operations stop being maintenance and become changes with a blast radius. Budget them like deployments, and set the size at which you will refuse new indexes before you reach it.
Postmortem

The disruption budget held the pod, the load balancer let it go

AssumptionA pod disruption budget protects availability during a scaling event.
What happenedA Redis config change raised CPU reservations, a pod became unschedulable, and an operator-initiated scaling event triggered an eviction. In GitLab's words: the PDB correctly blocked the pod eviction, but GKE's load balancer controller removed the NEG because NEGs are removed independently of PDB. The pod stayed running (PDB worked), but became unreachable via the ILB.
Blast radius4 hours 53 minutes. At least 53,726 namespaces affected on the CI path, 24,543,552 requests, and a complete outage of every Duo feature in both the editor extension and the web interface.
FixRollback of the configuration change; ArgoCD access pre-granted to on-call engineers, which the review names as a delay.
Design ruleTwo controllers that both decide whether a pod serves traffic, using different inputs, is a partition waiting to happen. Test the interaction, not each controller.
Postmortem

The load shedder that fed the queue it was draining

AssumptionDeferring a misbehaving job class reduces pressure on the queue store.
What happenedAudit event streaming jobs were deferred as a mitigation. The deferred jobs accumulated in Redis, exhausted memory, produced kernel errors and broke the Redis Sentinel quorum. The Sidekiq backlog passed 12 million jobs.
Blast radiusSeverity 1 across web, CI runners and Git, with error rates peaking at 5% for CI runners. One week later the same worker and the same shard produced a three-hour near repeat, because the corrective actions had not shipped yet.
FixSwitch from deferring to dropping; double the Redis memory and CPU; a feature change lock holding the area until a circuit breaker and a streaming buffer land.
Design ruleDeferral moves work into the resource you are trying to protect. Any shedding mechanism whose backlog lives in the saturated store is an amplifier; the only safe shed is a drop, and you have to decide in advance what you are willing to lose.
Postmortem

The revert that could not be merged

AssumptionA rollback path is available during an incident.
What happenedDuring the February 2026 Redis incident, the fix was written quickly and then stalled: the mis-configuration revert wasn't able to be merged because pipelines couldn't be processed (due to the incident itself).
Blast radiusPart of the 4 hour 53 minute duration. A July 2023 site-wide outage shows the same coupling from the other direction: a restart of a stale infrastructure pipeline took the site down for 231 minutes and carried the labels blocks deployments and blocks feature-flags.
FixNot architectural. The incident review records it as a difficulty rather than a corrective action.
Design ruleEvery organisation that runs its own tooling on its own platform has this dependency; most have never tested it. Ask which of your emergency actions require the failing system to be healthy, and build one path that does not.

Figure 5 · The February 2026 failure path, including the blocked fix

GitLab CIWorkhorse / RailsRedis Sentinel podGKE controllersOperatorGitLab CIWorkhorse / RailsRedis Sentinel podGKE controllersOperatorraise CPU reservation forSentinel1pod becomesunschedulable2scaling event3eviction attempt4PDB blocks eviction, pod keepsrunning5LB controller removes NEGanyway6connections fail via internal LB7CI job processing stalls, Duounavailable8open revert of the config change9pipeline cannot run, revert cannot merge10
GitLab CIWorkhorse / RailsRedis Sentinel podGKE controllersOperatorGitLab CIWorkhorse / RailsRedis Sentinel podGKE controllersOperatorraise CPU reservation forSentinel1pod becomesunschedulable2scaling event3eviction attempt4PDB blocks eviction, pod keepsrunning5LB controller removes NEGanyway6connections fail via internal LB7CI job processing stalls, Duounavailable8open revert of the config change9pipeline cannot run, revert cannot merge10
The interesting arrow is the last one: the revert depends on the CI system that the incident has disabled. Reconstructed from the incident review of 4 February 2026.
Diagram source

What is missing from this corpus is as informative as what is in it. There is no public incident in which a completed architectural split failed in the way its designers feared. No postmortem describes a decomposed database losing consistency across the main and ci boundary, and none describes a cell failing and being contained. That absence has two readings: either the splits work, or the failure modes they introduce have not yet been exercised at a scale that produces an incident. The corpus cannot distinguish them, and an architect copying this design should assume the second until their own drain test says otherwise.

05

Numbers you can plan against

Every figure below is GitLab measuring its own system, with the date it was measured. There is no independent measurement of GitLab.com in this corpus.

MetricValueContextAs ofSource
Largest table, total on-disk size2,975 GBci_builds, 1,551 GB data plus 941 GB across 30 indexes; 22.7% of the whole database2021-06Design doc
Growth rate of that table300 GB / monthProjected to approach 5 TB by end of 2021 without intervention2021-06Design doc
Single index build time1.5–6 hOn the largest tables during busy periods; runs synchronously and blocks deployment2021-06Design doc
Physical table size target< 100 GBIncluding indexes; action expected from 10 GB2021-06Design doc
CI builds created per day> 5 MForecast at the time to reach 20 M/day in the first half of 20242021-01CI/CD Scaling
Cumulative CI builds stored2 bn1 bn passed February 2021, 2 bn February 20222022-02CI/CD Scaling
Serialised data in two columns600 GB + 300 GBci_builds.options and ci_builds.yaml_variables, user-provided content2021-02CI/CD Scaling
Memory held per in-flight clone4–6 GBgit-pack-objects for a gitlab-org/gitlab clone; caps concurrent clones per node regardless of CPU2026-06Scaling Git
Repository open latency before GitalyP99 > 30 sAgainst roughly 15 ms of CPU time, which is what identified the filesystem as the cause2016Gitaly design
Production services monitored117Definition files; 14 Redis, 5 Memorystore, 4 PostgreSQL clusters, 4 pgbouncer fleets, 6 examples or aggregates2026-09Metrics catalogue
Kubernetes clusters in production4With similarly configured staging clusters; datastores excluded from Kubernetes2026-09Production architecture
Rails codebase2.2 M linesRuby only; the frontend is a separate single Webpack bundle2026Monolith decomposition
Worst recorded Sidekiq backlog> 12 M jobsCaused by a deferral mitigation from a previous incident2026-05Incident
Redis memory growth before intervention35 GiBOn the catchall_b Sidekiq shard during the May 2026 near-miss2026-05Incident review
Largest single incident blast radius in corpus24.5 M requestsOver 4 h 53 m, at least 53,726 namespaces on the CI path2026-02Incident review
Cells headroom requirement10x (high), 100x (medium)Stated as prioritised requirements, not as achieved figures2026-09Cells goals
Read these carefully

All of the above are measured by GitLab and reported by GitLab; none is independently verified. The forecast figures in the CI scaling document (20 million builds a day by 2024) are projections made with Prophet in 2021, and the corpus contains no document saying whether they were met. Three quantities are unknown and worth naming: fleet sizes per service, saturation headroom (GitLab's forecasting output is classified confidential under its own data standard, so the method is public and the values are not), and cost of any kind. If you need a cost comparison, this corpus cannot give you one.

The planning number an architect should actually carry away is not on the table, because it is a ratio rather than a measurement. GitLab's index build time crossed the length of its deployment window somewhere around a table size of one terabyte, and the response was a target four times smaller than that with intervention starting at one hundredth of it. If you want one heuristic from this decade, it is that the size at which a table becomes a release-engineering problem is roughly an order of magnitude below the size at which it becomes a query-performance problem, and you will hit the first one first.

06

The evidence wall

Every source behind this page, graded and filterable. All of them are GitLab's own primary record; the reason for that, and what it costs the guide, is stated below the wall.

Postmortem GitLab2016-07

Postgres DB Split brain incident led to data loss

The earliest detailed postmortem in the tracker. Two databases both took writes for six hours after an unalerted Pacemaker failover, and the losing node's writes were discarded deliberately to restore a single primary.

Carry forwardAlert on the failover event itself, not only on the symptoms it is supposed to prevent.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/48
Postmortem GitLab2017-02

Hourly LVM snapshots of the production databases

One of a cluster of issues filed on 1 February 2017, the day after a database incident. It states that the existing 24-hour snapshot has proven to be inadequate as a recovery technique, which dates the moment GitLab's backup posture changed.

Carry forwardRecovery point objectives are discovered during recovery, not during design. Test the restore, not the backup.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/113
Postmortem GitLab2017-02

Update PS1 across all hosts to differentiate hosts and environments

Filed alongside the snapshot issues. The corrective action is a shell prompt: show the environment in colour and show more of the hostname, so an engineer on a console knows which machine they are on.

Carry forwardSome of the highest-value corrective actions are interface changes for humans under stress, and they are the first to be dismissed as trivial in a design review.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/111
Case study GitLab2018-07

GCP Migration: short maintenance window to test fixes from the dry run

The 2018 move off Azure was rehearsed with announced maintenance windows. The test plan reads as a list of things that went wrong in the first dry run, including closing and reopening the front door correctly and verifying that it closed.

Carry forwardThe rehearsal artefact is more useful than the migration announcement: it names what the team did not trust.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/366
Postmortem GitLab2018-08

No automated wal-e database backups in gprd

Three days after the migration, automated base backups were absent in the new environment and nothing alerted. Found by an engineer who happened to look at the restore host.

Carry forwardEvery corrective action needs an alert that fires when the corrective action stops existing.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/414
Postmortem GitLab2021-05

Long-running CREATE INDEX blocking vacuum

A near miss recorded in plain language: twelve hours from a full database shutdown because an index build had been running for five and a half hours and vacuum could not proceed.

Carry forwardTransaction ID wraparound is a capacity limit with a deadline, and large tables are how you reach it accidentally.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4633
Postmortem GitLab2023-07

Site-wide outage triggered by restart of stale terraform pipeline

231 minutes of 503s, root-caused to a configuration change. Its labels are as informative as its body: blocks deployments and blocks feature-flags, the two levers an incident response needs most.

Carry forwardA stale pipeline is a loaded change waiting for someone to press restart. Expire them, or make restart require re-planning.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/15997
Postmortem GitLab2026-02

Incident review: Redis deployment error, CI delays and Duo outage

The clearest single document in the corpus. It contains the PDB and NEG disagreement, the measured blast radius, and the sentence about the revert that could not be merged because the pipelines were down.

Carry forwardList the emergency actions that depend on the system you would be fixing, and give at least one of them a path that does not.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/21208
Postmortem GitLab2026-05

Error rates violating SLO (Redis Sidekiq memory exhaustion)

A severity 1 in which the stated cause is an earlier incident's mitigation: deferred audit event jobs accumulated until Redis memory was exhausted and the cluster lost quorum, with a backlog above 12 million jobs.

Carry forwardWrite the expiry condition into every mitigation at the moment you apply it, because the mitigation is now a component.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/22103
Postmortem GitLab2026-05

Incident review: Redis primary CPU saturation approaching capacity limit

A near-repeat one week after the severity 1, with the same worker and the same shard, because the corrective actions had not yet shipped. It also identifies the feedback loop: deferring the job made Sidekiq itself the dominant source of new enqueues.

Carry forwardThe window between an incident and its corrective action is a known-vulnerable period; treat it as an active risk with an owner, not as project backlog.
https://gitlab.com/gitlab-com/gl-infra/production/-/issues/22170
Decision record GitLab2021-06

Limit on-disk table size to under 100 GB for GitLab.com

The pivot document of the decade. It sets a numeric target, lists the top 30 tables with sizes, ties specific incidents to specific tables, and spawns two ADRs that turn the target into a static-analysis rule.

Carry forwardTurn the scaling limit you discovered into a lint rule in the codebase. Guidance decays; a failing pipeline does not.
https://gitlab.com/gitlab-com/content-sites/handbook/.../database_size_limits
Decision record GitLab2021-01

CI/CD Scaling

Records the integer primary-key ceiling on ci_builds as an existential deadline for December 2021, the two options considered for build queuing, and the decision to move serialised columns to a metadata table to slow growth.

Carry forwardAudit your oldest tables for the framework defaults they were created with. The default that was fine in 2012 is the outage in 2021.
https://gitlab.com/gitlab-com/content-sites/handbook/.../ci_scale
Decision record GitLab2026-09

Cells design document (Protocells)

The fourth iteration of the cell architecture, narrowed to one goal: reducing load on the legacy cell's database. Carries a decision log of 28 ADRs, one marked obsolete, and a future-scope section listing what the earlier iterations promised and this one defers.

Carry forwardA multi-year platform programme becomes deliverable when its goal gets smaller. Write down which of the original promises you are deferring, and where.
https://gitlab.com/gitlab-com/content-sites/handbook/.../cells
Decision record GitLab2026-09

Cells: goals, glossary and requirements

Requirements written as a prioritised table, including 10x headroom at high priority and 100x at medium, and cost per user similar or lower to GitLab.com as an explicit constraint on the design.

Carry forwardGive headroom a number and a priority in the requirements, so the architecture can be argued against evidence rather than taste.
https://gitlab.com/gitlab-com/content-sites/handbook/.../cells/goals.md
Decision record GitLab2026-09

Cells ADR 001: routing technology using Cloudflare Workers

A complete ADR: two proofs of concept, one chosen, the alternatives named, and the consequences accepted in writing including increased vendor lock-in and a requirement knowingly unmet.

Carry forwardName the requirement your chosen option fails and its priority. That single line is what makes an ADR auditable two years later.
https://gitlab.com/gitlab-com/content-sites/handbook/.../001_routing_technology.md
Decision record GitLab2026-09

Rejected: stateless router using request buffering

A full design kept after rejection, with the reason preserved: cells bounce requests they cannot serve, which requires buffering and allows a request to be processed more than once.

Carry forwardKeep the rejected design in the repository next to the accepted one. It is the cheapest institutional memory available.
https://gitlab.com/gitlab-com/content-sites/handbook/.../rejected/buffering-requests
Decision record GitLab2026-09

Transaction management in Gitaly (status: rejected)

A design that was implemented and is now being deleted. The header explains that rejected is the closest available status and that the work was largely built and is now being unwound, along with the features that depended on it.

Carry forwardGive your design document a status field that can express "built and withdrawn". Most templates cannot, which is why most organisations lose this information.
https://gitlab.com/gitlab-com/content-sites/handbook/.../gitaly_transaction_management
Decision record GitLab2026-06

Scaling Git (proposal)

The third attempt at horizontal Git scaling, and the document that declares the second one failed. Proposes object storage as the source of truth with stateless Gitaly nodes and a content-addressed MVCC format, and names agentic workloads as a rising load source.

Carry forwardIf two attempts to replicate state have failed, the next move is usually to stop replicating and start sharing. The cost is latency; the gain is that any node can serve anything.
https://gitlab.com/gitlab-com/content-sites/handbook/.../scaling-git
Decision record GitLab2024-11

Siphon: CDC from PostgreSQL to a queue

Moves analytical consumers off the transactional database by streaming logical replication events into a pub/sub layer, with an explicit non-goal of letting the application emit custom events.

Carry forwardForbidding custom events keeps the change stream honest: every event has a row behind it, so the stream cannot drift from the database.
https://gitlab.com/gitlab-com/content-sites/handbook/.../siphon
Decision record GitLab2022-12

Use Zoekt for code search

Keeps Elasticsearch for issues and comments and adds a purpose-built engine for code, on the grounds that code search needs exactness and regex support that the general engine does not provide.

Carry forwardOne search engine for two query models is a false economy. Split by query semantics, not by data type.
https://gitlab.com/gitlab-com/content-sites/handbook/.../code_search_with_zoekt
Decision record GitLab2026-09

Rails monolith decomposition

Proposed in 2023 and still proposed in 2026, now with a section headed "the agentic imperative" arguing that modular boundaries are a prerequisite for agents, which cannot carry the implicit context a long-tenured engineer carries.

Carry forwardWatch for the rationale under a long-lived proposal changing. It tells you which argument the organisation now finds persuasive.
https://gitlab.com/gitlab-com/content-sites/handbook/.../modular_monolith
Source GitLab2021-06

Draft: add Database Sharding blueprint (closed, never merged)

The sharding design of 2021, opened as a draft merge request and closed unmerged. The approach that eventually shipped instead is domain decomposition followed by the Cells programme.

Carry forwardA closed, unmerged design MR is a dated record of a direction not taken. Search for them before you propose the same thing.
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64115
Source GitLab2026-08

Remove Raft, WAL and transaction code (epic 23151)

The removal programme for the Gitaly transactional cluster, with the order of work and the reason: the code is dead weight: a large surface to maintain. One constituent merge request touches 365 files.

Carry forwardBudget the removal when you budget the build. The cost of unwinding a partially adopted architecture is measured in review capacity, not in engineering days.
https://gitlab.com/groups/gitlab-org/-/epics/23151
Source GitLab2026-09

Gitaly design document

The original argument for putting an RPC layer in front of Git, with the measurement that motivated it and the alternative that was refused. Still in the repository ten years later, now describing a system whose storage model is being replaced.

Carry forwardWhen latency is two orders of magnitude above CPU time, the answer is in the storage path, and no amount of application tuning will find it.
https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md
Source GitLab2026-09

Multiple databases (development documentation)

Documents the live decomposition into main, ci and sec, and the application-level gitlab_schema classification that enforces it, including why PostgreSQL schemas were not used.

Carry forwardIf the database cannot enforce the boundary cheaply, enforce it in the application and make the classification mandatory per table.
https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/database/multiple_databases.md
Source GitLab2026-09

GitLab.com metrics catalogue

117 service definitions in jsonnet, which is the most precise public statement of GitLab.com's topology: every Redis family, every PostgreSQL cluster, the topology service, Zoekt, Siphon and ClickHouse all appear as first-class services.

Carry forwardA monitoring catalogue in version control is an architecture diagram that cannot go stale, because an unmonitored service is an outage.
https://gitlab.com/gitlab-com/runbooks/-/tree/master/metrics-catalog/services
Source GitLab2026-09

Sidekiq sharding runbook

The operational procedure for adding a Redis shard and moving workers onto it, including why Redis Cluster was not used and the recommendation of one queue per shard.

Carry forwardHot-key distribution decides whether clustering helps. Measure the key distribution before choosing a sharding technology.
https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/sidekiq/sharding.md
Case study GitLab2026-09

Capacity planning for GitLab infrastructure

Describes classifying every monitored resource by whether it scales horizontally, forecasting saturation daily with Prophet through a tool called Tamland, and filing the warnings as issues on a public board.

Carry forwardClassify resources as horizontally scalable or not, and let that classification set the priority of the warning. It is the cheapest way to stop treating every saturation alert as equally urgent.
https://gitlab.com/gitlab-com/content-sites/handbook/.../capacity-planning.md
Case study GitLab2026-09

Production architecture (controlled document)

The audited description of what runs GitLab.com: Google Cloud us-east1, four production Kubernetes clusters, and datastores deliberately outside them.

Carry forwardWrite down which components are exempt from your platform standard and why. The exemption list is where the operational risk lives.
https://gitlab.com/gitlab-com/content-sites/handbook/.../production/architecture
Case study GitLab2026-09

Deployments and releases

Establishes the cadence that the database size target was protecting: changes deploy to GitLab.com multiple times per day from packages built off the default branch, and self-managed packages follow monthly.

Carry forwardState your deployment cadence as a constraint in database design reviews. It is the number that decides whether a migration is acceptable.
https://gitlab.com/gitlab-com/content-sites/handbook/.../deployments-and-releases
What this evidence base is, and is not

Thirty documents on the wall, drawn from a ledger of thirty-three, one host, one organisation. There are no engineering blog posts, conference talks, papers or vendor case studies in this guide. The network policy applied to the session that produced it resolved gitlab.com and refused every other host, including GitLab's own blog, documentation site and status page. Rather than cite pages that could not be opened, the hunt was run entirely against what answered. The result is a corpus weighted almost entirely toward the three highest-value tiers, incidents, decision records and source, and completely missing the outside view. Every performance number here is GitLab measuring GitLab. Treat the mechanisms as well evidenced and the outcomes as self-reported.

07

Build a miniature, then productionise it

Six rungs. The line between toy and production-shaped is at rung four, where you stop adding capacity and start removing work.

Reproduce the constraint that started it all

Load a table to 50 GB in PostgreSQL with five indexes, then build a sixth index without CONCURRENTLY while a write workload runs. Time it. Then do it with CONCURRENTLY and time the vacuum behind it.

Done when: you can state your own index build time as a function of table size, in minutes.  Teaches: why a size target, rather than a query budget, is the enforceable control.

Turn the number into a rule the pipeline enforces

Write a check that fails CI when a migration adds a column to a table above your threshold, or an index to a table above half of it. Wire it into the same job that runs your linters, with an explicit override that requires a named approver.

Done when: a deliberately oversized migration is rejected by CI and the override path is documented.  Teaches: the difference between a policy and a control.

Decompose one domain out of a shared database

Take a service with one PostgreSQL database and move one bounded context to a second database. Classify every table. Find every cross-domain join and foreign key you have to break, and write down what replaces each one.

Done when: both databases run with no cross-database transactions and the classification is enforced in code.  Teaches: that decomposition costs are paid in the application layer, not the database layer.

Put a routing plane in front of two tenants

Run two complete copies of the application, each with its own database, and a small stateless router that maps tenant to instance through a lookup service. Keep the router under a few hundred lines. Measure the added latency at the edge.

Done when: a tenant can be moved between instances and the router picks up the change without a deploy.  Teaches: that the hard part of cells is the authoritative map, not the routing.

Break the mitigation on purpose

Build a job queue on Redis, add a deferral mechanism for a misbehaving job class, then make the deferral re-enqueue through the same queue. Watch memory. Now replace deferral with a drop and a counter.

Done when: you have a graph showing the deferral path growing without bound and the drop path flat.  Teaches: the May 2026 failure class, which is the one most likely to be in your own system already.

Cut the circular dependency in your own emergency path

List every action your incident response needs: revert a config change, disable a feature flag, drain a zone, roll back a deploy. For each, write down which systems must be healthy. Then take the most coupled one and build a path that bypasses it.

Done when: one emergency action can be completed with your CI system deliberately switched off.  Teaches: the lesson GitLab recorded in February 2026 and had not yet converted into a corrective action.

08

Keep hunting

This guide was assembled almost entirely through one API rather than a search engine. The queries below are the ones that produced it, and they work against any public GitLab instance.

A public incident corpus, by severity and year

  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?labels=incident,severity::1&state=closed&per_page=60&order_by=created_at&sort=desc"
  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?labels=incident-review&state=all&per_page=20&sort=desc"
  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?state=all&per_page=10&order_by=created_at&sort=asc"

Design documents, including the rejected ones

  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fcontent-sites%2Fhandbook/repository/tree?path=content/handbook/engineering/architecture/design-documents&per_page=100"
  • curl "https://gitlab.com/gitlab-com/content-sites/handbook/-/raw/main/content/handbook/engineering/architecture/design-documents/cells/_index.md"
  • grep -l "status: rejected" *.md

Directions abandoned rather than announced

  • curl "https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/merge_requests?state=closed&search=blueprint&per_page=50"
  • curl "https://gitlab.com/api/v4/groups/gitlab-org/epics/23151"
  • curl "https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/64115"

Topology you can count instead of guess

  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Frunbooks/repository/tree?path=metrics-catalog/services&per_page=100"
  • curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction"
  • curl "https://gitlab.com/api/v4/groups/gitlab-com%2Fgl-infra/projects?per_page=100&simple=true"

Two notes for anyone repeating this. Discussion notes on merge requests and issues require authentication, so the argument inside a thread is not reachable anonymously even when the issue body is; plan to work from descriptions, labels and linked epics. And the tracker's own format changed over the decade, from hand-written timelines in 2016 to structured incident.io exports from 2025 onward, which means a text search tuned to one era silently misses the other.

09

References

  1. GitLab, Postgres DB Split brain incident led to data loss - postmortem GitLab production tracker, 12 July 2016. Checked 2026-09-20.
  2. GitLab, Update PS1 across all hosts to more clearly differentiate between hosts and environments GitLab production tracker, 1 February 2017. Checked 2026-09-20.
  3. GitLab, Hourly LVM snapshots of the production databases GitLab production tracker, 1 February 2017. Checked 2026-09-20.
  4. GitLab, GCP Migration: Short Maintenance Window to Test Fixes from July 21 Findings GitLab production tracker, 26 July 2018. Checked 2026-09-20.
  5. GitLab, 2018-08-14 No automated wal-e database backups in gprd GitLab production tracker, 14 August 2018. Checked 2026-09-20.
  6. GitLab, 2021-05-18: Long-running CREATE INDEX blocking vacuum GitLab production tracker, 18 May 2021. Checked 2026-09-20.
  7. GitLab, 2021-06-06: CREATE INDEX operation on Postgres causing high network saturation GitLab production tracker, 6 June 2021. Checked 2026-09-20.
  8. GitLab, 2023-07-07: Site-wide outage triggered by restart of stale terraform pipeline GitLab production tracker, 7 July 2023. Checked 2026-09-20.
  9. GitLab, 2026-02-03: Redis deployment error causes delayed CI processing and GitLab Duo outage GitLab production tracker, 3 February 2026. Checked 2026-09-20.
  10. GitLab, Incident Review: Redis deployment error causes delayed CI processing and GitLab Duo outage GitLab production tracker, 4 February 2026. Checked 2026-09-20.
  11. GitLab, 2026-05-12: Error rates violating SLO GitLab production tracker, 12 May 2026. Checked 2026-09-20.
  12. GitLab, Incident Review: Redis primary CPU saturation on redis-sidekiq nodes approaching capacity limit GitLab production tracker, 20 May 2026. Checked 2026-09-20.
  13. GitLab, Database Scalability: Limit on-disk table size to < 100 GB for GitLab.com GitLab handbook design document, created 23 June 2021. Checked 2026-09-20.
  14. GitLab, CI/CD Scaling GitLab handbook design document, created 21 January 2021. Checked 2026-09-20.
  15. GitLab, Draft: Add Database Sharding blueprint (merge request 64115, closed unmerged) GitLab merge request, opened 15 June 2021. Checked 2026-09-20.
  16. GitLab, Multiple Databases GitLab development documentation, master branch. Checked 2026-09-20.
  17. GitLab, Decompose the GitLab application database into multiple databases (work item 6168) GitLab work item. Checked 2026-09-20.
  18. GitLab, Cells design document GitLab handbook design document, created 7 September 2022, current revision 2026. Checked 2026-09-20.
  19. GitLab, Cells: Goals, Glossary and Requirements GitLab handbook design document. Checked 2026-09-20.
  20. GitLab, Cells ADR 001: Routing Technology using Cloudflare Workers GitLab handbook decision record. Checked 2026-09-20.
  21. GitLab, Proposal: Stateless Router using Requests Buffering (rejected) GitLab handbook design document. Checked 2026-09-20.
  22. GitLab, Proposal: Stateless Router with Routes Learning (rejected) GitLab handbook design document. Checked 2026-09-20.
  23. GitLab, Gitaly design Gitaly repository, master branch. Checked 2026-09-20.
  24. GitLab, Transaction management in Gitaly (status: rejected) GitLab handbook design document, created 30 May 2023, marked rejected 2026. Checked 2026-09-20.
  25. GitLab, Remove Raft, WAL and transaction code (epic 23151) GitLab epic, created 7 August 2026. Checked 2026-09-20.
  26. GitLab, Scaling Git (proposal) GitLab handbook design document, created 16 June 2026. Checked 2026-09-20.
  27. GitLab, GitLab.com service metrics catalogue GitLab runbooks repository, master branch. Checked 2026-09-20.
  28. GitLab, Sidekiq Sharding runbook GitLab runbooks repository, master branch. Checked 2026-09-20.
  29. GitLab, Siphon design document GitLab handbook design document, created 20 November 2024. Checked 2026-09-20.
  30. GitLab, Use Zoekt for code search GitLab handbook design document, created 28 December 2022. Checked 2026-09-20.
  31. GitLab, Rails Monolith Decomposition GitLab handbook design document, created 22 May 2023, status proposed. Checked 2026-09-20.
  32. GitLab, Capacity Planning for GitLab Infrastructure GitLab handbook. Checked 2026-09-20.
  33. GitLab, Production Architecture (controlled document) GitLab handbook. Checked 2026-09-20.
  34. GitLab, Deployments and Releases GitLab handbook. Checked 2026-09-20.

Numbered in the order they first appear. The evidence ledger in sources.md uses its own numbering, one row per claim rather than one per document, so a document supporting three claims appears three times there and once here.