How Figma's Databases Team Lived to Tell the Scale

How Figma sharded its Postgres stack in place with DBProxy, hash routing, and Postgres views instead of migrating to a distributed SQL engine.

How Figma's Databases Team Lived to Tell the Scale

The obvious move when your Postgres tables hold billions of rows is to migrate to a distributed database. Figma looked at that path with only months of runway left and decided the riskiest thing it could do was leave Postgres at all.

Source

How Figma’s databases team lived to tell the scale

By: Sammy Steele, Figma
Published: Mar 14 2024
Type: Engineering article

This is worth saving because it shows the whole decision log behind a sharding migration — the options considered and rejected, not just the winner. It also walks through a small team scaling a relational stack in place, without rewriting the application around a new engine.

How Figma sharded Postgres in place

Figma’s database stack grew almost 100x between 2020 and this post. In 2020, everything fit on a single Postgres database hosted on AWS’s largest physical instance. By the end of 2022, the team had added caching, read replicas (copies of the database that serve reads so the main instance doesn’t have to), and a dozen vertically partitioned databases — groups of related tables, like everything about Figma files or organizations, split into their own databases.

That bought years of runway, but it had a floor. Vertical partitioning’s smallest unit is a single table, and some of Figma’s tables had grown to several terabytes and billions of rows. At that size, Postgres’s vacuum — the background cleanup that recycles old row versions and keeps the database from running out of transaction IDs — started causing reliability problems during its runs. The highest-write tables were about to exceed the input/output operations per second (IOPS) ceiling of Amazon’s managed RDS service. No amount of rearranging tables fixes one table that’s too big, so Figma needed a bigger lever.

Before picking one, the team wrote down what the lever had to do: minimize work for product developers, make future scale-outs invisible to the application, avoid backfills of huge tables (their throughput would have turned those into months of copying), roll out incrementally, stay reversible, keep strong consistency without double-writes, and lean on the expertise they already had running RDS Postgres. Read as a list, it’s less an architecture spec than a survival plan. Nearly every goal is about surviving the migration itself, which tells you where the real risk in this project lived.

Then came the alternatives: CockroachDB, TiDB, Spanner, and Vitess — the distributed “NewSQL” systems that shard for you. All were rejected, not on features but on migration risk. Moving their most business-critical data between two different database engines, while rebuilding operational expertise from scratch, on a timeline of months, was the bigger gamble. NoSQL was rejected because Figma’s data model is deeply relational; adopting it would have meant rewriting most of the backend. What remained was building horizontal sharding — splitting a table’s rows across many physical databases, routed by a key — on top of the Postgres they already ran.

That choice came with deliberate shrinkage. Figma’s version wouldn’t support atomic transactions across shards, would only handle a subset of Postgres, and would keep the door open to rolling back to unsharded Postgres if unknown unknowns appeared. The costs are real: queries must carry the shard key so they can be routed, schema changes must be coordinated across shards, foreign keys and globally unique indexes stop being enforceable, and a write spanning shards can half-succeed. Their example of the last one is moving a team between organizations and finding half its data missing. Even with a narrow first target — one simple, very high-traffic table — the whole effort took about nine months.

The design centers on a few choices. Tables were sharded in groups called “colos” (colocations), which share a shard key and physical layout; within one shard key value, a colo still supports joins and full transactions, which matched how most application code already talked to the database. Instead of forcing one universal shard key, Figma picked a handful — UserID, FileID, OrgID — that between them cover almost every table. Routing uses the hash of the key rather than the key itself, because their auto-incrementing and Snowflake-style IDs would have piled new rows onto one hot shard. The cost is that range scans over a shard key become inefficient, a pattern the article says is rare in their codebase.

The riskiest idea in the post is also the most reusable: separating logical sharding from physical sharding. Logically, a table becomes “sharded” while its data still sits on one host, by representing each shard as a Postgres view — a saved query that behaves like a table holding just the rows whose hashed key falls in that shard’s range. Reads and writes go through the views behind feature flags, and a bug can be undone in seconds by routing traffic back to the base table. Only after that topology is proven does the physical split happen: copy the data from one host to N, then flip traffic over. The team validated the view overhead with load tests and a shadow-reads framework comparing view and non-view queries on live traffic — minimal in most cases, under 10% in the worst. The first physical shard cutover, in September 2023, cost ten seconds of partial availability on primaries, no impact on replicas, and no latency or availability regressions.

Serving all this required a new component: DBProxy, a Go service sitting between the application and PgBouncer, their existing connection pooler. A pooler keeps a small set of database connections and lends them to incoming requests, because opening a fresh connection for every request is slow and a database can only hold so many. DBProxy’s query engine parses SQL into a tree, plans it against logical shards, then rewrites and routes it to physical databases, with extras like dynamic load-shedding and request hedging. Queries carrying the shard key execute on one shard; queries without it trigger a “scatter-gather,” fanning out to every shard and merging results — which loads the fleet as much as an unsharded query would, so too many of them would erode the scaling win. To decide which SQL to support, Figma ran “shadow planning” over live production traffic, logged the plans, and picked a subset covering roughly 90% of queries — joins allowed only within a colo, and only on the shard key. A topology service keeps DBProxy’s map of tables, shard keys, and databases current with updates in under a second.

What’s left is the long tail: shard every remaining table, and solve sharded schema updates, globally unique IDs, atomic cross-shard transactions, distributed unique indexes, a friendlier ORM layer, and one-click resharding. The team also expects to reassess the NewSQL options once the deadline pressure is gone.

My take

What the article reveals — beyond the architecture — is that the hard part isn’t sharding; it’s sequencing. Logical-before-physical via views turns one irreversible, terrifying operation into two independently reversible ones: first change how the application behaves (flagged, rolled back in seconds), then move the data (against a topology already proven in production). That’s the same shape as any safe rollout — deploy behind a flag, migrate after — applied at the database layer, and it generalizes far beyond Postgres.

I’d also argue the discipline of building less is what made in-house viable. A query engine for 90% of queries is a project; a query engine for all of Postgres is a database company. The generic tools Figma declined exist precisely because most teams shouldn’t build this — Figma’s excuse was that adopting one cost the migration it couldn’t afford. Worth noting the enabling condition, though: the access patterns cluster around user/file/org, with few range scans and no cross-shard transactions in the sharded subset. A workload full of analytics scans or cross-entity transactions couldn’t buy scalability this cheaply, and the article is honest that those remain unsolved on its road ahead.

Where this is useful

  • When a Postgres table is heading past the size where vacuum, index maintenance, and IOPS behave — this maps the options between “buy a bigger instance” and “migrate everything.”
  • When evaluating distributed SQL systems under deadline pressure — Figma’s criteria (backfill cost, reversibility, expertise rebuild) transfer to any migration decision.
  • When designing any risky data migration — the logical/physical split is a template: change behavior behind a flag first, move data second.
  • Less applicable when the workload needs atomic cross-shard transactions or heavy range scans — those are exactly what Figma gave up.

Apply this: Take your largest table and list which queries include a natural shard key (user, file, tenant, org) and which don’t. That list is the true cost of sharding — it’s what a scatter-gather path would have to absorb.

What I want to remember

Remember: Separate the logical change from the physical one. Figma made the application behave as if sharded while the data still sat in one place, so the risky physical split ran against an already-proven topology. Reversibility, not cleverness, is what let a small team shard a production database live.