GitLab, 2016–2026  / field guide
Practitioner field guide · 2026-09-25

Your singletons choose where you fail: ten years of GitLab's architecture, from its own record

GitLab publishes 207 architecture design documents and 172 labelled incident reviews in repositories anyone can read. Reading a decade of them together shows an architecture that kept decomposing the parts that were large while every serious outage arrived through the parts that were shared. This guide reconstructs the decisions, the rejected ones included, and turns them into rules you can apply to a platform of your own.

35 primary sources 1 company, read in depth 5 incident reviews Evidence through September 2026 Read: 20 min
01

The territory

A product designed to be installed once per company, operated instead as one shared instance for everybody, for ten years.

State the problem without the company's name and it is one most platform teams will recognise. You have an application whose data model assumes a single installation: one identifier space, one queue, one repository filesystem, one relational database holding every tenant's rows in the same tables. Then you sell it as a service. Every tenant you add makes the shared parts bigger, and none of them makes the shared parts more divisible. At some point growth stops being a capacity question and becomes a shape question.

GitLab has been living that problem in public since before 2016, and it is unusually useful to study because the working-out is published rather than narrated. The company's architecture design documents sit as Markdown in a Git repository, 207 of them, each with a status field and a list of alternatives considered. Rejected designs are kept on purpose: the Cells set has a rejected/ directory whose documents say, in the file itself, that they are retained “so that we can document the reasons for not choosing this approach”. Production incidents get a review issue in a public project, 172 of them carrying the incident-review label, with customer impact, request counts and root cause filled into a template.

2.2M
lines of Ruby in the single Rails application, with hundreds of engineers in it daily
2.5 TB
on-disk size of one table, ci_builds, growing 300 GB a month
12M
jobs backlogged when the shared Redis for Sidekiq failed
4
successive designs for the same horizontal-scaling programme, three now superseded
The finding that surprised me

The horizontal-scalability programme changed its own goal. Cells opened in September 2022 as “a new architecture for our software as a service platform… horizontally scalable, resilient, and provides a more consistent user experience”. Four iterations later the current one, Protocells, is described as “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”. A platform re-architecture narrowed, over four years and in its own words, into a database relief programme. That is not a criticism; it is the most honest thing in the corpus, and it is what a multi-year decomposition actually looks like from the inside.

What this guide covers: the request path, the storage layers underneath it, and the tenancy boundary, between roughly 2016 and September 2026, as evidenced by GitLab's design documents, decision records, repository contents and public incident reviews. What it deliberately does not cover: GitLab's self-managed and Dedicated products except where they constrain the SaaS design, its AI features, its commercial history, and any comparison with how other companies solved the same problem. It also does not cover GitLab's own blog posts. This session's network policy blocked every host except gitlab.com and github.com, so the guide is built entirely from material inside repositories. That is a real limitation and it cuts both ways: no narrative gloss, but also no independent account and no dissenting voice.

Figure 1 · Where a request meets something shared

Any tenant's
request

Cloudflare edge
(shared, third party)

Rails monolith
2.2M lines

PostgreSQL
(shared)

Redis for Sidekiq
(shared)

Gitaly RPC

Repository on one
storage node's disk

Sidekiq workers

Any tenant's
request

Cloudflare edge
(shared, third party)

Rails monolith
2.2M lines

PostgreSQL
(shared)

Redis for Sidekiq
(shared)

Gitaly RPC

Repository on one
storage node's disk

Sidekiq workers

Notice that the four components marked as shared are the four that produced the incidents in section 4. Reconstructed from the Cells goals document and the Gitaly README.
Diagram source
02

How the shape actually changed

Four layers moved over the decade, at four different speeds, and the order they moved in is the interesting part.

Git storage moved first and is still moving. Repositories used to be read straight off a shared filesystem by the Rails application. Gitaly put an RPC boundary in front of that, and the migration is complete in the sense the README states flatly: “All application code accesses Git repositories via Gitaly.” Its goal line is “Fault-tolerant horizontal scaling of Git storage in GitLab, and particularly, on GitLab.com”. The fault-tolerance half went through Praefect, described in the repository's HA design document as “a transparent front end to all Gitaly shards” that routes gRPC calls and runs quorum votes across replicas. The horizontal-scaling half is not finished. In June 2026 a new proposal, Scaling Git, opens by conceding the original constraint is still in force: “Gitaly stores the authoritative copy of a repository on the serving nodes' filesystem. Compute and storage are thus tightly coupled with one another, which makes it hard to scale either of these dimensions.” Its answer is object storage as the single source of truth with stateless Gitaly nodes over a local cache, which depends on Git's pluggable object database work being upstreamed first.

The relational layer moved on a number. In June 2021 a design document set a target that reads like an operational rule rather than an architecture principle: physical tables on GitLab.com under 100 GB including indexes, with action starting at 10 GB. It is worth noticing what that replaces. “Split the monolith” is unfalsifiable and never finishes; “this table is 2.5 TB and grows 300 GB a month” produces a queue of work with an order. The decade's database output, decomposition into separate main and CI databases, partitioning, retention, and now change-data-capture into ClickHouse through a component called Siphon, is what that queue looks like when it is worked.

The analytical load left by replication rather than by service extraction. Siphon, designed in November 2024, “delivers serialized CDC (change data capture) data from the PostgreSQL logical replication stream to a queueing system” for consumers to load into ClickHouse or Snowflake. The notable choice is the non-goal: it is PostgreSQL only, and it is explicitly “mostly transparent to application developers making changes to the Rails application”. Analytics was moved off the transactional database without asking the monolith's authors to change how they write.

The tenancy boundary moved last, and it is the one that forced everything else. The Cells goals document does not hedge: “GitLab.com operates as a single monolithic instance with a shared database, which creates a fundamental scalability bottleneck”, and the estimate behind it names the two resources that cannot be fixed by more of the previous decade's work: PostgreSQL and Redis are “non-horizontally scalable resources, even when database partitioning and decomposition are taken into account”. That sentence is the hinge of the whole ten years. Partitioning buys time on the tables; it does not divide the instance.

Figure 2 · The target shape, and the part that stays singular

Cells, AWS us-east-1

Control plane, GCP

Cloudflare edge

org to cell lookup

organization migration

Routing Service
(Worker, target ~1000 LOC)

Topology Service

Cloud Spanner

Cell A
Rails + Sidekiq + Gitaly + PG

Cell B
same, independent

Legacy cell
(today's GitLab.com)

Cells, AWS us-east-1

Control plane, GCP

Cloudflare edge

org to cell lookup

organization migration

Routing Service
(Worker, target ~1000 LOC)

Topology Service

Cloud Spanner

Cell A
Rails + Sidekiq + Gitaly + PG

Cell B
same, independent

Legacy cell
(today's GitLab.com)

Organizations are the logical boundary and Cells the physical one, but the Topology Service that maps between them is itself a global singleton, now on managed Spanner in GCP while the first cells run in AWS. Reconstructed from the Cells design document, ADR 015 and ADR 019.
Diagram source

Figure 3 · One programme, four designs, a narrowing goal

2022-09: programme opens
horizontally scalable, resilient,
more consistent user experience

Cells 1.0

Cells 1.5

Cells 2.0

2026: Protocells
permanently reducing load on
the legacy cell database

2022-09: programme opens
horizontally scalable, resilient,
more consistent user experience

Cells 1.0

Cells 1.5

Cells 2.0

2026: Protocells
permanently reducing load on
the legacy cell database

Notice the wording at each end: the same programme opens as a platform re-architecture and currently describes itself as database relief. From the Cells design document.
Diagram source

Organization, not tenant

The logical boundary had to be invented before anything could be split, because the data model had no unit of ownership big enough to move. Organizations became that unit, and the rule that makes it work is stated as a constraint on code, not on infrastructure: cross-organization operations must go through public APIs.

Evidence: Cells design document

Identifiers before data

Moving a tenant between cells is impossible while two cells can mint the same primary key. ADR 008 hands each cell a range of sequence values from the Topology Service at provisioning time, after recording five alternatives including composite keys and logical replication. Identity is the first thing a split breaks.

Evidence: Cells ADR 008

Cells arriving in the monolith

This is no longer only a design document. The main Rails repository now carries a cells-mailroom directory at its root, beside app and workhorse. The architecture is landing as code in the thing it is meant to divide, which is how these migrations always go and why they take years.

Evidence: repository tree, gitlab-org/gitlab

03

The decisions that matter

Three forks where the rejected option is written down, and the condition that would flip each one.

Decision: where does the tenant-routing layer run?

Chosen
  • A Cloudflare Worker, in JavaScript or TypeScript, doing an organization-to-cell lookup against the Topology Service
  • Chosen after proofs of concept against the alternatives, and expected to stay “very small and simple (up to 1000 lines of code)”
Rejected
  • Istio, judged “not the right fit”
  • Two stateless-router designs, one buffering and replaying requests, one learning routes through pre-flight requests
  • The stated cost of the buffering design: “each request might be sent more than once and be processed more than once as result”
Flips when
  • You need the same routing on installations you do not operate. The ADR records the consequence in advance: the stack “will not support all self-managed customers”, treated as a low-priority requirement
  • The routing layer stops being small. The whole bet is that a 1000-line component is cheap to rewrite, so the dependency is reversible

The line worth pausing on is not the choice but the consequence GitLab wrote next to it: “More vendor locking with Cloudflare, but are already heavily dependent on them.” That is an honest ADR and a common one. Twenty-two months later, on 5 December 2025, a Cloudflare outage took GitLab.com down for 25 minutes, affecting “All SaaS users” and roughly 155 million requests, and the incident review's follow-up actions include reducing the Cloudflare dependency. Both documents are correct. The gap between them is the thing to learn from: an accepted dependency is a decision to accept an outage you have not scheduled, and the ADR is the right place to say how big that outage may be, not merely that the dependency exists.

Decision: split the code, or split the instance?

Chosen, in practice
  • Split the instance. Cells has four generations of design, a decisions directory with 29 ADRs, infrastructure in a second cloud and code landing in the monolith
Not rejected, but not done
  • Splitting the code. The Rails Monolith Decomposition document was created on 2023-05-22 and still carries status: proposed in September 2026
  • Its 2026 revision re-argues the case from agentic development rather than from scale: modular boundaries are now “the structural prerequisite for agentic development at scale”
Flips when
  • Your bottleneck is a shared runtime resource rather than a shared codebase. Splitting code does not divide one PostgreSQL primary or one Redis
  • It flips back when the constraint is change velocity and merge contention, which is why the decomposition argument survives in a new costume

Read those two rows together and a rule falls out that applies well beyond this company. Code decomposition and runtime decomposition solve different problems and are routinely confused because both are called “breaking up the monolith”. GitLab's record shows the runtime split advancing through four designs and two clouds while the code split sat at proposed for three years. If your pain is a saturated shared resource, module boundaries will not touch it.

Decision: how do you make a stateful storage tier fault-tolerant?

Chosen, so far
  • Praefect in front of Gitaly shards, with quorum voting across replicas
  • A transactional layer with write-ahead logging, running on GitLab.com but still logging on startup that it “is not production ready yet and might lead to various issues including data loss”
Argued against, in the open
  • Merge request 8895, opened 2026-06-10, removes Raft, the write-ahead log and transactions across 365 files. It was closed without merging
  • Six days later, Scaling Git proposed replacing the premise instead: object storage as the source of truth, stateless compute, a content-addressed manifest
Flips when
  • You can put the durability somewhere that already solves replication. Consensus over local disks is the expensive answer to a question object storage answers for you, and it stops being worth it the moment your latency budget tolerates a fetch

That closed merge request is the most interesting artefact in the corpus, and it needs stating carefully. Nothing in it says the transactional layer is being abandoned; it is an exploratory branch, and it was closed. What it demonstrates is that removing several years of consensus machinery was concrete enough for someone to produce the diff, in the same month that a proposal appeared to solve the same problem a different way. Consensus-over-disks and object-storage-as-truth are competing answers to one question, and this repository is one of the few places you can watch the competition rather than read its result.

Figure 4 · Which decomposition does your problem need

no, it is just big

yes

yes

no, it is the codebase

not yet

yes

Is the saturating resource
shared by all tenants
and undividable?

Is it one table
or one dataset?

Can tenants be
assigned an owner
in the data model?

Set a size ceiling
and partition to it

Module boundaries;
expect no runtime relief

Build the ownership unit first;
nothing else can start

Split the instance:
routing, identifiers, migration

no, it is just big

yes

yes

no, it is the codebase

not yet

yes

Is the saturating resource
shared by all tenants
and undividable?

Is it one table
or one dataset?

Can tenants be
assigned an owner
in the data model?

Set a size ceiling
and partition to it

Module boundaries;
expect no runtime relief

Build the ownership unit first;
nothing else can start

Split the instance:
routing, identifiers, migration

The terminal nodes are actions, and the first question is the one most teams skip. Derived from the sequence of work evidenced across GitLab's design documents.
Diagram source
DecisionChosenRejectedBecauseEvidence
Tenant routingCloudflare Worker over Topology ServiceIstio; request buffering; routes learningPoC results; buffering meant duplicate executionCells ADR 001
Cluster-wide identifiersPer-cell bigint sequence ranges issued at provisioningGlobal claim service; composite keys; logical replicationTenant mobility requires non-overlapping keys with no runtime coordinationCells ADR 008
Routing control-plane storeCloud Spanner, multi-region configuration under testReusing the existing PostgreSQL estateNeeds high availability with strong consistency, globallyCells ADR 015
Where cells runAWS us-east-1, control plane left in GCPKeeping everything in one cloudSelected on measured cross-cloud latency to the Topology ServiceCells ADR 019
Analytics off the primaryLogical-replication CDC into ClickHouse or SnowflakeBespoke per-feature sync toolingOne standard extraction path, transparent to application developersSiphon, 2024-11
Deploying to many cellsA change-coordinator engine with priorities and ringsCI pipelines plus resource_group ordering, inherited from DedicatedAdequate at one tenant per merge request; not at cell scaleCoordinating changes in Cells, 2024-07
04

What broke, and what each one was really about

Five reviews from the public corpus, chosen because between them they cover every failure class in it. Not one of them is a bug in the 2.2 million lines of Ruby.

Group them and three classes account for all five. A shared runtime resource saturated by one workload (the Redis pair). A stateful node that owns everyone on it (the Gitaly incident). A change path reaching production where the request path could not (Consul, and arguably Cloudflare, since GitLab was downstream of somebody else's change). The classes are worth naming because they predict where your next incident comes from better than any component inventory does.

Postmortem

One worker's jobs filled the queue substrate for everybody

AssumptionDeferring a misbehaving worker's jobs is a safe mitigation, because deferral is backpressure.
What happenedAudit-event jobs deferred as mitigation for an earlier incident accumulated and consumed all memory on the Redis Sidekiq cluster; kernel errors followed, multiple nodes failed, and quorum broke.
Blast radiusSeverity 1. Error rates across web, CI runners and Git; more than a million requests affected; backlog over 12 million jobs; 1 hour 31 minutes of impact inside a 4 hour 15 minute incident, 2026-05-12.
FixRedis VMs resized to double memory and CPU; corrective work on the worker itself tracked under a feature change lock.
Design ruleDeferral is not backpressure. Deferring work moves it into the store that is already the constrained resource, so any queue-level mitigation must be able to drop, not only delay.
Postmortem

The same worker, a week later, stopped one step short

AssumptionThe previous week's resize bought enough headroom.
What happenedThe same audit-event worker surged again. Redis memory climbed to 35 GiB; the review notes that “Redis's single-threaded architecture limited CPU scaling”, so vertical headroom did not translate into throughput. Engineers switched from deferring jobs to dropping them.
Blast radiusSeverity 2 near miss. Audit events dropped globally for 3 hours 7 minutes, 2026-05-19. No wider degradation, because the team intervened before memory exhaustion.
FixDropping rather than deferring; the worker remains under a feature change lock.
Design ruleWhen a shared component's capacity is single-threaded, vertical scaling buys memory and not concurrency. Know which of the two your saturation is in before you resize.
Postmortem

One storage node took out one very large namespace

AssumptionAfter a decade of work, Git storage is a scaled-out tier rather than a set of individually load-bearing machines.
What happenedA single Gitaly storage node spiked in anonymous memory usage and degraded. Rebooting it as mitigation made it worse: the node failed to mount its data disk, needing console access and a filesystem repair.
Blast radiusSeverity 2. All requests in the gitlab-org namespace: 500s on merge requests, comments and reviews for 3 hours 28 minutes, 2026-03-24, with integrity checks continuing afterwards.
FixFilesystem repair and reboot. Structurally, the answer is the Scaling Git proposal published three months later.
Design ruleSharding by tenant makes the largest tenant a single point of failure with extra steps. Check the distribution, not the mechanism: if one shard holds a namespace everybody depends on, you have partitioned the machines and not the risk.
Postmortem

A local development tool reached production and removed Consul

AssumptionRunning a routine task from a development environment cannot change production.
What happenedIn the review's own words, an engineer triggered a routine task on a local development environment and, “due to an unfortunate series of events and unexpected tool behavior”, it changed production, taking down Consul, on which the Patroni PostgreSQL high-availability setup depends.
Blast radiusRoughly 100% of traffic. The CDN reported about 13 million requests returning 503, 2022-11-30. Detection was immediate; diagnosis followed the Patroni failover alert to the missing Consul cluster.
FixReinstalling the Consul release, then corrective actions on the tooling's environment targeting.
Design ruleYour blast radius is set by the widest thing your tooling can address, not by the widest thing your request path can reach. Partition the change path or accept that it is unpartitioned.
Postmortem

The dependency an ADR accepted, priced

AssumptionDepending further on an edge provider you already depend on adds no new risk.
What happenedA Cloudflare outage prevented access to GitLab.com and, notably, to the internal tools the responders needed: the review lists runbooks, dashboards and operational platforms among the things that were unreachable.
Blast radiusSeverity 1. “All SaaS users”, roughly 155 million requests, 25 minutes, 2025-12-05. Thirteen related alerts fired.
FixEscalation to the vendor; follow-up actions to reduce the Cloudflare dependency and improve outage documentation.
Design ruleA shared dependency that also carries your incident-response tooling is worse than a shared dependency. Keep runbooks and dashboards on a different failure domain from the product, and say in the ADR how long the outage may be, not only that the coupling exists.

Figure 5 · How a deferral became an outage

Every other tenantRedis (shared)SidekiqAudit event workerEvery other tenantRedis (shared)SidekiqAudit event workermitigation: defer jobsresize doubles memory,not concurrencyjob surgeenqueuedeferred jobs stored herememory exhausted,kernel errorsnodes fail, quorum lost12M job backlog, 500s everywhere
Every other tenantRedis (shared)SidekiqAudit event workerEvery other tenantRedis (shared)SidekiqAudit event workermitigation: defer jobsresize doubles memory,not concurrencyjob surgeenqueuedeferred jobs stored herememory exhausted,kernel errorsnodes fail, quorum lost12M job backlog, 500s everywhere
The mitigation for the first incident is the mechanism of the second: deferred jobs are stored in the resource that was already saturating. From the 2026-05-12 review.
Diagram source
05

Numbers you can plan against

All measured and published by GitLab, with the date each was measured, because several are now years old and the growth rates are the point.

MetricValueAtContextAs ofSource
Target ceiling, physical table size100 GBGitLab.comIncluding indexes; action starts at 10 GB2021-06Design doc
Largest table, on disk2.5 TBGitLab.comci_builds: 1.5 TB data, 1 TB across 31 indexes2021-06Design doc
Growth of that table300 GB/moGitLab.comForecast to approach 5 TB within the year if untouched2021-06Design doc
CI builds created>5 M/dayGitLab.comNamed as the reason database limits were slowing development2021-01CI/CD Scaling
Cumulative builds stored1 B → 2 BGitLab.com1 billion by 2021-02-01, 2 billion by 2022-022022-02CI/CD Scaling
Stated growth target20 M/dayGitLab.com“Enable future growth by making processing 20M builds in a day possible”2021-01CI/CD Scaling
Redis memory at intervention35 GiBredis-sidekiqSingle-threaded, so CPU did not scale with the resize2026-05Incident review
Peak Sidekiq backlog12 M jobsGitLab.comPipeline creation went near zero; >10,000 pipelines affected2026-05Incident review
Requests lost, worst full outage~13 MGitLab.com503s reported by the CDN; about 100% of traffic2022-11Incident review
Requests affected, edge outage~155 MGitLab.com25 minutes, all SaaS users2025-12Incident review
Refs in a large repository866 kAndroid, upstream GitWhy ref storage needed replacing; packed-refs scans linearly2026-09git reftable doc
Public record size207 / 172GitLabDesign documents / issues labelled incident-review2026-09-25Issue list
Read these carefully

Every figure here is measured and published by the organisation that runs the system, which makes them credible about their own systems and silent about anyone else's. None is a vendor claim and none is independently verified. The database figures are from June 2021 and are certainly stale by now; treat them as a growth rate and a ratio, not as a current state. The number to carry is not 2.5 TB, it is 300 GB a month against a 100 GB ceiling, which tells you the ceiling was being crossed roughly every four months by one table alone. That is the arithmetic that justifies a decade of partitioning work, and it is the arithmetic to do on your own largest table this week.

06

The evidence wall

Everything this page rests on, graded. Sixteen decision records, five incident reviews, six code and repository artefacts, no vendor material and, for the reason given in section 1, no engineering blog posts.

Postmortem GitLab2026-05

Incident Review: Error rates violating SLO (INC-10096)

Severity 1. Deferred audit-event jobs exhausted Redis memory, broke cluster quorum and left a backlog of more than 12 million Sidekiq jobs across web, CI and Git.

Carry forwardA mitigation that defers work into the saturating store is not a mitigation.
gitlab.com/gitlab-com/gl-infra/production/-/work_items/22108
Postmortem GitLab2026-05

Incident Review: Redis primary CPU saturation on redis-sidekiq nodes

The near-repeat one week later, with the clearest statement in the corpus of why vertical scaling stopped working: Redis is single threaded, so the resize added memory and not concurrency.

Carry forwardClassify saturation as memory-bound or concurrency-bound before choosing a remedy.
gitlab.com/gitlab-com/gl-infra/production/-/work_items/22170
Postmortem GitLab2026-03

Incident review: Slow Gitaly operations and gitlab-org 500 errors

One storage node degraded, then failed to mount its data disk after a mitigation reboot, removing an entire namespace for three and a half hours.

Carry forwardReboot as a first mitigation on a stateful node can convert degradation into unavailability.
gitlab.com/gitlab-com/gl-infra/production/-/work_items/21629
Postmortem GitLab2025-12

Incident Review: GitLab.com is down (INC-6113)

An upstream edge outage removed the product and the responders' own runbooks and dashboards at the same time. Quantified: all SaaS users, about 155 million requests, 25 minutes.

Carry forwardIncident tooling must not share a failure domain with the product it is used to recover.
gitlab.com/gitlab-com/gl-infra/production/-/work_items/20942
Postmortem GitLab2022-11

Incident review for 2022-11-30: GitLab.com site-wide outage

A routine task run from a development environment changed production and removed Consul, on which Patroni depends. About 13 million 503s.

Carry forwardThe change path is usually the least partitioned path you own.
gitlab.com/gitlab-com/gl-infra/production/-/work_items/8100
Decision record GitLab2022-09

Cells (design document, four iterations)

The programme document itself, revised through 2026. Records that Protocells replaces Cells 1.0, 1.5 and 2.0, and restates the goal as relieving the legacy cell's database.

Carry forwardWrite the iteration history into the design document; it is the only honest record of scope.
handbook repo › design-documents/cells/_index.md
Decision record GitLab2022–2026

Cells: Goals

Names the constraint outright: PostgreSQL and Redis are non-horizontally-scalable resources even after partitioning and decomposition, and shared infrastructure produces noisy-neighbour effects between top-level groups.

Carry forwardWrite down which resources cannot be divided by the work you are already doing.
handbook repo › cells/goals.md
Decision record GitLab2023–2024

Cells ADR 001: Routing Technology using Cloudflare Workers

Chooses an edge runtime over Istio and two internal designs, and records the consequences in plain language, including further vendor lock-in and no self-managed support.

Carry forwardAn ADR that lists consequences it dislikes is more useful than one that only justifies.
handbook repo › cells/decisions/001
Decision record GitLabrejected

Proposal: Stateless Router using Requests Buffering (rejected)

The obvious design, kept in a rejected/ directory. Its recorded cost is that a request may be sent and processed more than once.

Carry forwardPublish rejected designs in the same tree as accepted ones; the reason is the asset.
handbook repo › cells/rejected/buffering
Decision record GitLabrejected

Proposal: Stateless Router using Routes Learning (rejected)

The second rejected router, requiring routable information encoded in the URI and a pre-flight request per route.

Carry forwardRouting designs differ mainly in where they put the cost of not knowing the tenant yet.
handbook repo › cells/rejected/routes-learning
Decision record GitLab2023–2026

Rails Monolith Decomposition

2.2 million lines of Ruby, created 2023-05-22, still status: proposed, with the 2026 revision re-arguing the case from agentic development rather than scale.

Carry forwardA design document that stays "proposed" for three years is telling you the pain is elsewhere.
handbook repo › modular_monolith
Decision record GitLab2021-06

Database Scalability: limit on-disk table size to <100 GB

The measurable target that organised a decade of database work, with the table inventory that justified it.

Carry forwardReplace "the database is too big" with a number and a threshold for acting early.
handbook repo › database_size_limits
Decision record GitLab2021-01

CI/CD Scaling

Traces a 2012 data model to a 2021 constraint, with build volumes, cumulative counts and a stated target of 20 million builds a day.

Carry forwardDate your data model. The year a table was designed predicts which limit you hit first.
handbook repo › ci_scale
Decision record GitLab2026-06

Scaling Git

Proposes object storage as the source of truth with stateless Gitaly compute and an MVCC manifest, conceding that compute and storage are still coupled today.

Carry forwardWhen durability can move to object storage, consensus over local disks stops earning its cost.
handbook repo › scaling-git
Decision record GitLab2024-11

Siphon

Change data capture from PostgreSQL logical replication into a queue, for ClickHouse and Snowflake consumers, deliberately transparent to application developers.

Carry forwardMoving analytical load can be a replication problem rather than a service-extraction problem.
handbook repo › siphon
Decision record GitLab2024-07

Coordinating changes in Cells

Deployment engine inherited from the Dedicated product, based on CI pipelines, judged insufficient once changes must be sequenced and prioritised across many cells.

Carry forwardCellular architecture turns release engineering into a scheduling problem you must design.
handbook repo › cells/infrastructure/managing_changes
Decision record GitLab2025-05

Cells ADR 019: AWS primary region selection

First cells provisioned in AWS us-east-1 while the Topology Service stays in GCP, chosen on measured cross-cloud latency.

Carry forwardCross-cloud is a latency budget decision before it is a procurement one.
handbook repo › cells/decisions/019
Decision record GitLab2025-05

Cells ADR 015: Cloud Spanner region configuration

The Topology Service needs high availability with strong consistency, so the routing control plane runs on managed Spanner rather than the existing PostgreSQL estate.

Carry forwardThe lookup that makes sharding work is itself a global strongly-consistent store. Budget for it.
handbook repo › cells/decisions/015
Decision record GitLab2024–2026

Cells ADR 008: cluster-wide unique database sequences

Sequence ranges issued per cell at provisioning, with five alternatives recorded and linked to the discussion threads that produced them.

Carry forwardFix identifier uniqueness before tenant mobility, not during it.
handbook repo › cells/decisions/008
Decision record GitLab2025-05

Cloudflare Standardization across GitLab

Terraform modules to standardise a dependency that several teams had already adopted independently for DNS, WAF and Workers.

Carry forwardDependencies spread by convenience and get standardised afterwards; the standardisation document is where you can still price the risk.
handbook repo › cloudflare-standardization
Decision record GitLab2023-09

Capacity planning (Tamland)

Saturation forecasting as an owned artefact that predicts SLO violations and their dates, extended from GitLab.com to Dedicated tenants.

Carry forwardForecast the date a resource saturates, not the current utilisation. Only the date schedules work.
handbook repo › capacity_planning
Source GitLab2026-06

Merge request 8895: “Remove raft, wal, transactions” (closed, unmerged)

A 365-file change deleting Raft, the write-ahead log and the transaction manager from Gitaly. Opened 2026-06-10, closed without merging, six days before the object-storage proposal appeared.

Carry forwardRejected and abandoned branches are where a project's live architectural argument is visible.
gitlab.com/gitlab-org/gitaly/-/merge_requests/8895
Source GitLab2026-09

Gitaly: doc/transactions.md

Transactions run on GitLab.com behind a flag and log, on startup, that the feature is not production ready and “might lead to various issues including data loss”.

Carry forwardRead the warning strings in a dependency's code; they are the most candid maturity statement available.
gitlab.com/gitlab-org/gitaly/-/blob/master/doc/transactions.md
Source GitLab2026-09

Gitaly: doc/design_ha.md

Praefect as a transparent front end over Gitaly shards, with the vocabulary of accessors, mutators, voters and quorum that the transactional design is built on.

Carry forwardA proxy that also runs votes is two components; failure analysis should treat them separately.
gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md
Source GitLab2026-09

Gitaly README

States the completed half of the migration, “All application code accesses Git repositories via Gitaly”, against the goal that is still open, fault-tolerant horizontal scaling of Git storage.

Carry forwardThe boundary can be finished years before the scaling it was meant to enable.
gitlab.com/gitlab-org/gitaly/-/blob/master/README.md
Source GitLab2026-09

cells-mailroom/ in the main Rails repository

Cell-specific code sitting at the root of the monolith, beside app, workhorse and sidekiq_cluster.

Carry forwardCheck the repository tree, not the roadmap, to find out how far an architecture has actually landed.
gitlab.com/gitlab-org/gitlab/-/tree/master/cells-mailroom
Source Git project2026-09

git: Documentation/technical/reftable

The upstream work GitLab's storage plans depend on. Quantifies why ref storage is a scaling problem: 866,000 refs in Android, a 62 MB packed-refs file scanned linearly and rewritten for a two-ref transaction.

Carry forwardWhen your storage plan depends on upstream work, the upstream document is part of your risk register.
github.com/git/git › reftable
Source GitLab2026-09-25

The incident-review issue list

172 labelled reviews in a public project, each on a template with customer impact, request counts, timeline and root cause. This is the corpus the failure section is drawn from, and it is queryable through the REST API.

Carry forwardIf a vendor publishes incident reviews as issues, you can measure their reliability rather than reading their status page.
gitlab.com/gitlab-com/gl-infra/production › work items, label incident-review
07

Build a miniature, then productionise it

Six rungs. The line between a toy and something production-shaped is crossed at rung four, where you stop building the split and start testing it.

Inventory the singletons

List every runtime resource in your system that all tenants share and that cannot be divided by work already in flight. Next to each, the saturation metric and the shape of its ceiling: memory, concurrency, disk, connections.

Done when: the list is shorter than ten items and someone disagrees with one of them.  Teaches: that "shared" and "large" are different problems.

Put a date on the nearest ceiling

Take your largest table or busiest shared store, measure its growth rate, and compute the month it crosses a threshold you are willing to name. GitLab's ratio is the model: 300 GB a month against a 100 GB target.

Done when: a calendar date exists and is wrong by a defensible margin.  Teaches: forecasting schedules work; utilisation does not.

Invent the ownership unit

Find the entity in your data model that could own a tenant's data completely. If none exists, that is the project: everything downstream needs it. Write the rule that cross-unit access must go through a public interface.

Done when: you can name, for any row in your largest table, the unit that owns it.  Teaches: why tenancy work starts in the data model and not in the infrastructure.

Route two instances and make identifiers disjoint

Stand up two copies of the application with separate databases, a lookup service mapping unit to instance, and a thin router in front. Give each instance a non-overlapping identifier range at provisioning time.

Done when: a request reaches the right instance and a row created on either can be inserted into the other without collision.  Teaches: that the router is easy and the identifier space is not.

Move a tenant, then break the move

Migrate one unit between instances with no data loss, then deliberately leave a dangling reference: a comment by a user who did not move. Render both sides.

Done when: pages on source and target still render with references missing.  Teaches: that graceful degradation of cross-boundary references is the real cost of splitting, not the copying.

Saturate the shared store on purpose, then kill the router

Flood one tenant's jobs into your shared queue and watch every other tenant. Then take the router or its lookup service away and measure what remains available. Do both with your runbooks hosted somewhere else.

Done when: you can name the first mechanism that degrades, and your availability ceiling is written as a product of cells, router and third parties.  Teaches: that a cellular architecture inherits a new global singleton, and that recovery tooling belongs in a different failure domain.

The lesson to carry into your own system

Ten years of this company's record point one way. You do not choose where your architecture fails; your singletons choose, and they keep choosing right through a decomposition programme because decomposition removes the large things long before it removes the shared ones. So order the work by divisibility rather than by size: find the resources every tenant touches and no partition scheme reaches, name them in writing, and treat each one as an outage you have already agreed to have. The ones you cannot remove, such as a routing control plane or an edge provider, are not exceptions to that rule. They are the next chapter of it, which is why the incident review that mattered most here is the one where the responders could not reach their own runbooks.

08

Keep hunting

The queries that produced this page. They are API calls rather than search strings, which is the point: for a company that publishes its record as repository data, the API is the search engine.

Incidents, as data

  • https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?labels=incident-review&per_page=50
  • https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?search=Incident%20Review&in=title&state=closed&sort=asc
  • curl -sSI '.../issues?labels=incident-review&per_page=1' | grep x-total

Decisions, including the rejected ones

  • .../projects/gitlab-com%2Fcontent-sites%2Fhandbook/repository/tree?path=content/handbook/engineering/architecture/design-documents&per_page=100
  • tree?path=...design-documents/cells/rejected
  • tree?path=...design-documents/cells/decisions

The argument nobody published

  • .../projects/gitlab-org%2Fgitaly/merge_requests?state=closed&order_by=updated_at
  • .../merge_requests/8895/notes?sort=asc

What actually shipped

  • .../projects/<ns%2Fproject>/repository/tree?path=&per_page=100
  • .../repository/files/<url-encoded-path>/raw?ref=main

Two habits transfer to any organisation, not only this one. First, look for a rejected/ or superseded/ directory before you read the accepted design; the reasons stored there are worth more than the conclusion. Second, when a vendor's incident reviews are issues rather than status-page entries, you can count them, group them by service label and read the recurrence, which is a far better input to a procurement decision than an availability percentage.

09

References

  1. GitLab, Cells design document GitLab handbook repository, created 2022-09-07, revised through 2026. Checked 2026-09-25.
  2. GitLab, Cells: Goals GitLab handbook repository. Checked 2026-09-25.
  3. GitLab, Cells ADR 001: Routing Technology using Cloudflare Workers GitLab handbook repository. Checked 2026-09-25.
  4. GitLab, Cells ADR 008: Cluster wide unique database sequences GitLab handbook repository. Checked 2026-09-25.
  5. GitLab, Cells ADR 015: Cloud Spanner Region Configuration for Topology Service GitLab handbook repository, 2025-05-08. Checked 2026-09-25.
  6. GitLab, Cells ADR 019: AWS Primary Region Selection for Cells Infrastructure GitLab handbook repository, 2025-05-08. Checked 2026-09-25.
  7. GitLab, Proposal: Stateless Router using Requests Buffering (rejected) GitLab handbook repository. Checked 2026-09-25.
  8. GitLab, Proposal: Stateless Router using Routes Learning (rejected) GitLab handbook repository. Checked 2026-09-25.
  9. GitLab, Coordinating changes in Cells GitLab handbook repository, 2024-07-16. Checked 2026-09-25.
  10. GitLab, Rails Monolith Decomposition GitLab handbook repository, created 2023-05-22, status proposed. Checked 2026-09-25.
  11. GitLab, Database Scalability: Limit on-disk table size to <100 GB for GitLab.com GitLab handbook repository, 2021-06-23, status accepted. Checked 2026-09-25.
  12. GitLab, CI/CD Scaling GitLab handbook repository, 2021-01-21. Checked 2026-09-25.
  13. GitLab, Scaling Git GitLab handbook repository, 2026-06-16, status proposed. Checked 2026-09-25.
  14. GitLab, Siphon GitLab handbook repository, 2024-11-20. Checked 2026-09-25.
  15. GitLab, Cloudflare Standardization across GitLab GitLab handbook repository, 2025-05-20. Checked 2026-09-25.
  16. GitLab, Capacity planning for GitLab Dedicated (Tamland) GitLab handbook repository, 2023-09-11. Checked 2026-09-25.
  17. GitLab, Incident Review: Error rates violating SLO (INC-10096) gitlab-com/gl-infra/production, 2026-05-12. Checked 2026-09-25.
  18. GitLab, Incident Review: Redis primary CPU saturation on redis-sidekiq nodes gitlab-com/gl-infra/production, 2026-05-20. Checked 2026-09-25.
  19. GitLab, Incident review: Slow Gitaly operations on gitlab.com and gitlab-org 500 errors gitlab-com/gl-infra/production, 2026-03-24. Checked 2026-09-25.
  20. GitLab, Incident Review: GitLab.com is down (INC-6113) gitlab-com/gl-infra/production, 2025-12-05. Checked 2026-09-25.
  21. GitLab, Incident review for 2022-11-30: GitLab.com site-wide outage gitlab-com/gl-infra/production, 2022-11-30. Checked 2026-09-25.
  22. GitLab, issues labelled incident-review gitlab-com/gl-infra/production. 172 issues carried the label as of 2026-09-25, counted through the REST API; the web list is filterable by label. Checked 2026-09-25.
  23. GitLab, Gitaly merge request 8895: Remove raft, wal, transactions (closed, unmerged) gitlab-org/gitaly, opened 2026-06-10. Checked 2026-09-25.
  24. GitLab, Gitaly: Transactions gitlab-org/gitaly, master. Checked 2026-09-25.
  25. GitLab, Gitaly High Availability (HA) Design gitlab-org/gitaly, master. Checked 2026-09-25.
  26. GitLab, Gitaly README gitlab-org/gitaly, master. Checked 2026-09-25.
  27. GitLab, cells-mailroom directory gitlab-org/gitlab, master. Checked 2026-09-25.
  28. Git project, Documentation/technical/reftable git/git, master. Checked 2026-09-25.