Scaling Past a Single Database
Through 2020, Figma's infrastructure team watched the service outgrow its original architecture. Database traffic was growing roughly 3x annually, driven by new features, a second product launch, and a steadily expanding user base. Most metadata—permissions, file info, comments—still lived in one large Amazon RDS PostgreSQL instance. That single machine handled core collaborative features well, but the numbers were trending in the wrong direction: CPU utilization regularly exceeded 65% at peak traffic, and latency became increasingly erratic as the database approached its limits.
Total saturation would have meant Figma going down entirely. The team wasn't close to that point, but their mandate is to address scalability risks before they become operational threats—without disrupting the product in the process.
Interim Measures
The first round of fixes bought roughly a year of runway while the team planned a broader solution:
- Upgrading to the largest available RDS instance type (r5.24xlarge from r5.12xlarge)
- Adding read replicas to offload read traffic
- Spinning up separate databases for new use cases so growth didn't accumulate on the primary
- Deploying PgBouncer as a connection pooler
Those changes helped but didn't solve the underlying problem. Analysis of query traffic showed that writes contributed heavily to database utilization, and many reads couldn't be redirected to replicas because the application was sensitive to replication lag. Both read and write workloads still needed to move off the original instance.
Why Not Shard?
The obvious path—horizontal sharding—didn't survive contact with the team's constraints. Most managed horizontally scalable databases aren't natively compatible with Postgres, which Figma uses as its DBMS. Migrating to a NoSQL store or Vitess (MySQL) would require a complex double-read/double-write migration, and NoSQL would also demand major application-side rewrites. The Postgres-compatible NewSQL options available were unproven at Figma's scale; adopting one would make Figma an early, very large customer for systems that hadn't been stress-tested at that level. Self-hosting was the fallback, but the team had built on managed services and lacked the operational expertise to run Postgres in-house—a significant cost that would distract from the scalability work itself.
Vertical Partitioning as a Path Forward
The alternative was vertical partitioning: instead of splitting every table across multiple databases, move groups of tables onto their own dedicated databases. This relieves pressure on the original instance immediately and, importantly, leaves the door open for horizontally sharding subsets of tables later.
Choosing which tables to move came down to two criteria:
- Impact: the tables must account for a substantial portion of workload
- Isolation: the tables shouldn't be tightly coupled to others
To measure impact, the team tracked average active sessions (AAS) per query, sampling pg_stat_activity every 10 milliseconds to attribute CPU waits to specific queries, then aggregating by table.
Isolation was trickier. Moving tables into a separate database forfeits atomic transactions across tables, foreign key validation, and joins—features the application had been relying on. Partitioning tables with strong dependencies would force significant rewrites across the codebase, so the team needed to find query patterns and tables that were weakly connected to the rest of the schema.
Static analysis couldn't surface those patterns. The backend is Ruby, and developers write queries through ActiveRecord, whose dynamic nature makes it hard to determine which physical tables a query touches without executing it. So the team built runtime validators that hooked into ActiveRecord and streamed production query data—caller location, tables involved, transaction context—to Snowflake. Analyzing that telemetry revealed which groups of tables were consistently queried together in expensive, high-volume workloads. Those clusters became the prime candidates for vertical partitioning.
Moving live data without going offline
Figma's real-time collaboration means the database can never go dark for maintenance. Any migration must happen while users keep working. That ruled out the typical offline migration playbook and forced the team to build a custom partitioning pipeline.
Three requirements shaped the design:
- Availability impact limited to under one minute
- A repeatable, automated procedure
- The ability to undo a partition after the fact
No off-the-shelf tool met all three, so engineering built one.
Why so many moving parts?
The difficulty isn't moving rows between databases. It's the thousands of application backend instances that hold connections and route queries. Coordinate them badly and queries hit the wrong database at the wrong time. To de-risk that, the team didn't start by moving data. They moved the connection layer first.
PgBouncer sits between every client and the database — security groups give only PgBouncers direct database access. By splitting the PgBouncer layer into separate services before any data changes, clients can be blindly routed to the "wrong" PgBouncer without failing, because both still point at the same target database. Runtime visibility from that layer lets engineers confirm the application is ready before the actual switch.



Only once each application talks to the correct PgBouncer and traffic flows as expected does the real partitioning begin.
Logical replication wins on flexibility
Postgres offers both streaming and logical replication. Figma chose logical replication for three reasons:
- Subset of tables. Migrate only the target tables, so the destination starts with a much smaller storage footprint and a reduced hardware surface.
- Cross-version. Replicate from one Postgres major version to another. That enables major upgrades via the same tooling — AWS blue/green deployments for RDS Postgres aren't available yet.
- Reverse direction. A stream running back to the original database is the rollback path.
The glaring downside was speed: with terabytes of production data, the initial copy could take days or weeks. The cause turned out to be index maintenance. Logical replication bulk-copies rows, but updates each destination index one row at a time. By dropping indexes in the destination before copying and rebuilding them afterward, copy time dropped to a few hours.
Logical replication also enabled the safety net. A reverse replication stream from the new database back to the original — activated immediately after the original stops receiving write traffic — means any new post-migration changes are continuously replicated back to the old database. Rollback is always possible.
The handoff
With thousands of client services hitting the database daily, coordinating the switch across hundreds of nodes would be a coordination nightmare. The two-phase design — PgBouncers first, then data — collapses the final critical operation down to coordinating only the handful of PgBouncer nodes on the partitioned path.
The switch itself follows a strict sequence:
- Suspend new traffic at the PgBouncer layer and revoke query privileges on the original tables.
- After a short grace period, cancel the few in-flight queries (usually fewer than 10, since Figma's queries are short-lived).
- Confirm the two databases are synchronized.
- Promote the replica, stop logical replication, and start the reverse stream for rollback support.
- Resume traffic, now pointed at the new database.
Synchronization is confirmed using LSNs (log sequence numbers). Sampling an LSN from the original once writes have stopped, then waiting for the replica to replay past that point, guarantees the data is identical on both sides — no writes can be lost in the transition.


Scaling past the first partition
The procedure has run in production many times since, each achieving the original goal: add capacity without reducing reliability. The first run migrated two high-traffic tables; the October 2022 operation handled 50. Each migration caused roughly 30 seconds of partial availability impact and about 2% dropped requests.
The headroom gained is dramatic. The largest partition sees CPU utilization around 10%, and some lower-traffic partitions run with reduced allocations.
Vertical partitioning, however, is not a permanent end state. Tables with very high write volumes or multi-terabyte footprints will eventually exhaust CPU, disk, or I/O on their own. Client-side routing complexity also rises multiplicatively as the database count climbs. Figma has since introduced a query routing service that centralizes that routing logic, and the tooling built for vertical splits is intended as the foundation for horizontal sharding of the hottest tables. The infrastructure now has the runway to keep up with current growth while building for what's next.



