<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <!-- Source: https://planetscale.com/blog/feed.atom -->
  <title>Blog — PlanetScale</title>
  <subtitle>Posts about the PlanetScale platform, MySQL, PostgreSQL, databases, and more.</subtitle>
  <link href="https://siftrss.com/f/y6MV6p117oA"/>
  <link rel="alternate" type="text/html" hreflang="en" href="https://planetscale.com/blog"/>
  <link rel="self" type="application/atom+xml" href="https://siftrss.com/f/y6MV6p117oA"/>
  <id>https://siftrss.com/f/y6MV6p117oA</id>
  <updated>2026-09-25T00:00:00.000Z</updated>
  <entry>
    <title>The architecture of Neki</title>
    <link href="https://planetscale.com/blog/the-architecture-of-neki"/>
    <id>https://planetscale.com/blog/the-architecture-of-neki</id>
    <published>2026-09-18T00:00:00.000Z</published>
    <updated>2026-09-18T00:00:00.000Z</updated>
    <author>
      <name>Harshit Gangal</name>
    </author>
    <category term="engineering"/>
    <category term="neki"/>
    <content type="html"><![CDATA[<p>Meet Neki: sharding for Postgres. Neki allows applications to connect to massive, sharded databases over a single connection string. This post takes apart the architecture from the bottom up, one piece at a time, starting with what&#x27;s underneath all of it.</p><h2 id="real-postgres"><a href="https://planetscale.com/blog/the-architecture-of-neki#real-postgres">Real Postgres</a></h2><p>Neki is built as a sharding and scaling solution for real Postgres. It&#x27;s not a fork, nor a wire-compatible reimplementation, nor a MySQL sharding idea wearing a Postgres label. Neki uses ordinary PostgreSQL instances that store rows in Postgres data pages using MVCC, carry out transactions, and work as you would expect with <code>psql</code> and other Postgres drivers. Neki builds around those instances to let you shard them, scale them, and manage them as one database.</p><p>Let&#x27;s take a look:</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-cluster-architecture-BdagkgJK.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-cluster-architecture-darkmode-YGx-3wht.svg?auto=compress%2Cformat"><img alt="The full Neki cluster: an application talking to the Router, two shards each with a primary and a replica running Sidecar, Postgres, and Replicator, PostgresManager controlling each Postgres instance, Admin and etcd forming the control plane, and an Operator provisioning every pod." src="https://planetscale-images.imgix.net/assets/neki-cluster-architecture-BdagkgJK.svg?auto=compress%2Cformat" width="1504" height="1058" loading="lazy"></picture></p><h2 id="postgresmanager"><a href="https://planetscale.com/blog/the-architecture-of-neki#postgresmanager">PostgresManager</a></h2><p>Using vanilla Postgres means Neki needs a way to run and manage each instance. That includes starting and stopping Postgres, owning its data directory, and configuring replication so a new instance can join a shard. <strong>PostgresManager</strong> handles this coordination, running as the first process in the Postgres container and managing the <code>postgres</code> process directly.</p><h2 id="sidecar"><a href="https://planetscale.com/blog/the-architecture-of-neki#sidecar">Sidecar</a></h2><p>Postgres uses a separate backend process for each connection and limits how many can be open at once. Neki’s <strong>Sidecar</strong> sits in front of each instance and pools connections, letting many client connections share fewer Postgres backends.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-sidecar-highlight-B8X81j7N.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-sidecar-highlight-darkmode-CNtJVItJ.svg?auto=compress%2Cformat"><img alt="A close-up of one Neki shard: the Router sends queries to a primary and replica, each with its own Sidecar highlighted in orange alongside Postgres, PostgresManager, and Replicator." src="https://planetscale-images.imgix.net/assets/neki-sidecar-highlight-B8X81j7N.svg?auto=compress%2Cformat" width="704" height="555" loading="lazy"></picture></p><p>The Router, which is the component that accepts external client connections, communicates with the Postgres nodes via these Sidecars.</p><p>It also reports each Postgres instance&#x27;s health and whether it is a primary or replica, so the rest of the cluster knows whether it can receive write queries.</p><p>The pool doesn&#x27;t treat every connection the same way. The length of time a connection is checked out for use varies depending on what it&#x27;s being used for. A multi-statement transaction holds on to its connection until <code>commit</code> or <code>rollback</code>. A session-scoped advisory lock needs a connection of its own, because the lock has to outlive whatever transaction is open at the time and can&#x27;t share that connection. Everything else checks a connection out and hands it back the moment the statement finishes.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-sidecar-pooling-tiers-DqP_9-5W.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-sidecar-pooling-tiers-darkmode-CsEWlGUM.svg?auto=compress%2Cformat"><img alt="Three connection-pool lifetimes in the Sidecar: a shared connection for an autocommit statement, a dedicated connection for an open transaction, and a reserved connection for a session advisory lock." src="https://planetscale-images.imgix.net/assets/neki-sidecar-pooling-tiers-DqP_9-5W.svg?auto=compress%2Cformat" width="1504" height="454" loading="lazy"></picture></p><p>The Sidecar knows which of the three to use because the Router sends the necessary information with the query: autocommit, an open transaction, or a session that has to stay on one backend.</p><h2 id="shards"><a href="https://planetscale.com/blog/the-architecture-of-neki#shards">Shards</a></h2><p>Each Postgres instance gets its own Sidecar and PostgresManager pair. Real deployments need more than one instance: a primary and its replicas. Neki calls that group a <strong>shard</strong>, the unit it splits data across. It&#x27;s always advised to run a shard with a primary and 2+ replicas for high availability, as well as for additional read query capacity.</p><p>A shard is considered one Postgres cluster. Its replicas are physical copies of the primary, so they share a catalog and the same object identifiers.</p><p>Object Identifiers (OIDs) are how Postgres tracks objects internally, rather than by name. A client reads a column’s type OID off the wire to interpret its bytes and may cache that OID for later re-use. A custom type therefore needs to carry the same OID no matter which shard answers the query. Independent shards can assign that type different OIDs, so Neki designates one shard in the entire Neki cluster as the <strong>authoritative shard</strong>. This shard is the source of truth for translating custom type OIDs in responses from other shards to match. It ensures OIDs are consistent across the many shards of the Neki cluster.</p><p>The authoritative shard&#x27;s Sidecar also watches for schema changes and reports them to the Routers. This keeps the Routers&#x27; view of the schema current when a table is renamed or a column is dropped.</p><h2 id="admin"><a href="https://planetscale.com/blog/the-architecture-of-neki#admin">Admin</a></h2><p>In a distributed system, instances can fail independently while the rest of the system lives on. Neki is no different. A primary or replica can go down at any moment while its fellow instances on the shard are healthy. The <strong>Admin</strong>&#x27;s job is to detect failures, promote a replica, and maintain each shard’s durability policy.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-admin-highlight-Bz1IxBf6.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-admin-highlight-darkmode-Uru2fPFH.svg?auto=compress%2Cformat"><img alt="The same cluster diagram with Admin and the control plane highlighted and everything else faded." src="https://planetscale-images.imgix.net/assets/neki-admin-highlight-Bz1IxBf6.svg?auto=compress%2Cformat" width="1504" height="1058" loading="lazy"></picture></p><p>It health-checks every Sidecar, tracks replication lag for each replica, and decides when a shard needs a new primary. When a primary goes down, it coordinates an emergency failover, promoting a replica to take its place. It can also coordinate a planned switchover, which are needed for intentional node resizes and version upgrades. In both situations, Admin uses <code>pg_rewind</code> to bring diverged instances onto the new primary’s timeline, copying only the data that changed since the timelines diverged.</p><p>Each shard has a durability policy that determines when a commit is acknowledged:</p><ul><li><strong>Async:</strong> The primary acknowledges the commit without waiting for a replica.</li><li><strong>Sync:</strong> The primary waits for a replica to confirm the commit, protecting against the loss of a single node.</li><li><strong>Cross-zone sync:</strong> The primary waits for confirmation from a replica in another availability zone, protecting against the loss of the primary’s zone.</li></ul><p>Postgres enforces whichever one is configured, using its own synchronous replication machinery. The Admin keeps that configuration correct as replicas join or leave shards, or a failover moves the primary to a different zone.</p><p>Much of Admin’s work, however, doesn’t involve changing the primary. It repoints replicas to the correct replication source and corrects roles when Postgres and the topology disagree.</p><h2 id="operator"><a href="https://planetscale.com/blog/the-architecture-of-neki#operator">Operator</a></h2><p>Neki’s components need to be deployed, updated, and replaced when their machines fail. Neki is built Kubernetes-first, and the <strong>Operator</strong> manages this full lifecycle.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-operator-highlight-BD94jQsB.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-operator-highlight-darkmode-8OihOEsZ.svg?auto=compress%2Cformat"><img alt="The same cluster diagram with the Operator highlighted and everything else faded." src="https://planetscale-images.imgix.net/assets/neki-operator-highlight-BD94jQsB.svg?auto=compress%2Cformat" width="1504" height="1058" loading="lazy"></picture></p><p>The Operator models a cluster as a hierarchy. A cluster owns routers and shards, and each shard owns the pods running its Postgres instances and Sidecars. When the Neki cluster configuration changes, the Operator works out which pods need to be created, updated, or removed.</p><p>How it replaces an instance depends on whether that instance is still running. For a live instance, the Operator builds a replacement and confirms it has caught up before deleting the old one. If a node fails and loses its ephemeral storage, the Operator rebuilds the lost instance from scratch once its safety checks pass.</p><p>Admin and the Router handle the database side of those disruptions. Admin coordinates a switchover for planned primary replacements or a failover when a primary goes down. The Router can buffer queries that are safe to retry while a healthy primary becomes available.</p><h2 id="router"><a href="https://planetscale.com/blog/the-architecture-of-neki#router">Router</a></h2><p>We&#x27;ve talked a lot about how the Neki cluster operates and handles failure internally. What we&#x27;ve yet to dive into is how applications use the thing!</p><p>The <strong>Router</strong> is the entry point for clients connecting to a Neki cluster, presenting a single Postgres wire-protocol endpoint to connect to a (potentially) massive sharded database. Applications use Postgres drivers to send SQL and open transactions without managing connections to individual shards.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-highlight-D4fCw8OU.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-highlight-darkmode-DzHaPeoU.svg?auto=compress%2Cformat"><img alt="The same cluster diagram with the Router and etcd highlighted and everything else faded." src="https://planetscale-images.imgix.net/assets/neki-router-highlight-D4fCw8OU.svg?auto=compress%2Cformat" width="1504" height="1058" loading="lazy"></picture></p><p>Authentication and role checks are done as if it were the Postgres instance itself, and the protocol&#x27;s own extended-query flow and prepared-statement lifecycle are all built into the Router.</p><p>Once a query arrives, the Router runs a Postgres-compatible parser against the authoritative shard&#x27;s catalog, plans it against the current sharding layout, and sends it to whichever Sidecar needs to run it over gRPC.</p><p>Not every query can run on a single shard. A join may need data from several shards or an aggregate may need to read from all of them. The <strong>Router</strong> coordinates that work as a <a href="https://planetscale.com/blog/what-is-a-neki-router">distributed query</a>.</p><p>Whenever possible, it leaves the work to the Postgres instances. If both sides of a join are on the same shard, the Router sends the join to that shard. When a join needs to run across shards, the Router executes it itself, choosing between nested-loop, hash, and merge joins based on cost estimations.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>Read more about Routers, parsing, and sharded query planning in our other blog, <a href="https://planetscale.com/blog/the-lifecycle-of-a-sharded-postgres-query">The lifecycle of a sharded Postgres query</a>.</p></div><p>Earlier, we covered how Admin promotes a new primary during a switchover or failover. If that happens, the Router can buffer queries, giving the Admin time to complete the handover. For queries that can safely be retried after failing against a primary, the Router buffers the query and waits, for a fixed time, for a healthy primary. Once a healthy primary is available, the Router releases queued queries gradually.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-buffering-DChcV18g.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-buffering-darkmode-HQltRPC0.svg?auto=compress%2Cformat"><img alt="A query hits an unavailable primary and gets buffered. Usually the primary recovers within the time and size limit and the queued query is released gradually, so the client sees nothing. Rarely, the limit is reached first and an error is returned to the client." src="https://planetscale-images.imgix.net/assets/neki-router-buffering-DChcV18g.svg?auto=compress%2Cformat" width="1504" height="500" loading="lazy"></picture></p><h2 id="data-topology"><a href="https://planetscale.com/blog/the-architecture-of-neki#data-topology">Data Topology</a></h2><p>Router, Sidecars, and Admin all need a consistent picture of which shards exist, what key ranges they own, and which tables are sharded at all. If the Router&#x27;s copy is wrong, a query can land on the wrong shard. This is all specified with a <a href="https://planetscale.com/blog/what-is-a-data-topology">Data Topology</a>, and <strong>etcd</strong> holds the single, authoritative copy of it. When the Data Topology changes, the Router, Sidecars, and Admin pick up the updated configuration without a restart or manual synchronization.</p><p>The Data Topology defines <strong>shard groups</strong>, named sets of physical shards, each owning a range of routing keys. Each table belongs to a shard group. <strong>Shard indexes</strong> specify the columns or expressions and the strategy used to turn row values into routing keys. Those keys determine which shard receives each row.</p><h2 id="replicator"><a href="https://planetscale.com/blog/the-architecture-of-neki#replicator">Replicator</a></h2><p>As a database grows, its layout may need to change. Tables need to be imported, shards need to be split, and schemas need to change all while applications keep using the database.</p><p>Neki&#x27;s <strong>Replicator</strong> handles the data movement behind all such operations. It runs as a separate process colocated with a shard&#x27;s Sidecar and Postgres. It is responsible for copying existing rows to new destinations, and also keeping the data current by decoding changes from a Postgres logical replication stream and applying them as SQL.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-replicator-highlight-flGISLaG.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-replicator-highlight-darkmode-D-xFVR2s.svg?auto=compress%2Cformat"><img alt="A close-up of one Neki shard, with the separate Replicator beside each Postgres instance and its connection from Postgres highlighted in orange." src="https://planetscale-images.imgix.net/assets/neki-replicator-highlight-flGISLaG.svg?auto=compress%2Cformat" width="704" height="385" loading="lazy"></picture></p><p>Three workflows use the Replicator:</p><ul><li><strong>MoveTables</strong> relocates a set of tables, including imports from an external Postgres instance</li><li><strong>Reshard</strong> redistributes data across shard key ranges, allowing a shard to be split when it outgrows its capacity</li><li><strong>OnlineDDL</strong> changes a table&#x27;s schema by building a shadow table alongside the original and keeping it current through the same change-data-capture pipeline MoveTables and Reshard use to relocate rows. A final rename swaps the new table into place. This supports changes such as repartitioning a table, alongside changes that would otherwise require a blocking operation.</li></ul><p>Once the data has been copied and the destination is caught up, the workflow switches from the original tables or shards to their replacements. This is the cutover. The Router uses the same buffering mechanism that handles primary changes for this step. It buffers queries during that switch and releases them afterward.</p><p>Together, these components let Neki scale Postgres horizontally while presenting a single database to applications.</p><h2 id="get-started"><a href="https://planetscale.com/blog/the-architecture-of-neki#get-started">Get started</a></h2><p>Neki is in Platform Preview right now.</p><p>Start a <a href="https://app.planetscale.com/new">Neki</a> cluster today: build on it from scratch, or import an existing Postgres database.</p>]]></content>
    <summary><![CDATA[Sharded Postgres, from the team behind Vitess]]></summary>
  </entry>
  <entry>
    <title>Introducing Neki</title>
    <link href="https://planetscale.com/blog/introducing-neki"/>
    <id>https://planetscale.com/blog/introducing-neki</id>
    <published>2026-09-10T12:00:00.000Z</published>
    <updated>2026-09-10T12:00:00.000Z</updated>
    <author>
      <name>Nick Van Wiggeren</name>
    </author>
    <category term="product"/>
    <category term="neki"/>
    <content type="html"><![CDATA[<p>Neki is now available in platform preview.</p><p><a href="https://neki.dev/">Neki</a> is built from lessons we’ve learned over eight years of running some of the largest sharded MySQL clusters in the world. Thousands of production workloads with millions of queries per second for companies where even a few seconds of downtime is a very public event. We know what it means to power the world’s biggest tier 0 workloads.</p><p>When we released PlanetScale Postgres a year and a half ago, we knew we needed to do more. In that time we’ve onboarded several thousands of customers on PlanetScale, some of them rivaling the size of our largest MySQL customers. Time and time again, we watched teams approach the ceiling of a single machine with Postgres. Metal bought them time, but with customers hitting the upper limit of what a single machine is capable of, we found there was no good option to hand them. Enter Neki.</p><h2 id="what-is-neki"><a href="https://planetscale.com/blog/introducing-neki#what-is-neki">What is Neki?</a></h2><p>Neki is sharded Postgres from PlanetScale. It lets you scale a Postgres database across many machines while keeping real Postgres on every shard.</p><p>Your application connects to a Neki router over the standard Postgres wire protocol, so your existing drivers, ORMs, and connection string keep working. Each shard is a full Postgres cluster with one primary and at least two replicas across 3 availability zones. There is no custom storage engine, so extensions, SQL support, and performance behave the way Postgres does.</p><p>You choose the shard key and control how tables are grouped and distributed through a JSON data topology. Schema changes, version upgrades, failovers, imports, and resharding all run as built-in fully online workflows. You also get the PlanetScale features you already rely on, including Insights, schema recommendations, branching, and MCP.</p><p>You don&#x27;t have to shard on day one. Run Neki as a single primary with replicas, and when you outgrow one machine, resharding is a workflow you run against the cluster you already have.</p><h2 id="why-neki"><a href="https://planetscale.com/blog/introducing-neki#why-neki">Why Neki?</a></h2><p>You already know the problems that come with fast-growing Postgres databases: tables too large to vacuum or index without affecting traffic, backups taking hours, connection limits, maintenance windows for schema changes, transaction wraparound and so much more.</p><p>You can move to a bigger instance, but eventually you run out of big enough machines, and the problems don’t scale linearly as you add more cores and IOPS.</p><p>The existing answers each ask you to give something up. Application-level sharding pushes routing into your code. Postgres-”compatible” distributed databases hide the shard key from you, take away your extensions, and add complexity and latency which becomes difficult to handle and debug.</p><p>So we built Neki with a few principles, the biggest one being: stick to Postgres, don’t work around it, fake it, or turn away from it.</p><h2 id="how-does-neki-work"><a href="https://planetscale.com/blog/introducing-neki#how-does-neki-work">How does Neki work?</a></h2><p>We architected Neki from first principles for Postgres, with real Postgres on every shard. There are four moving parts.</p><h3 id="neki-routers"><a href="https://planetscale.com/blog/introducing-neki#neki-routers">Neki routers</a></h3><p>Your application first connects to a Neki router. The router speaks the Postgres wire protocol so your existing drivers and ORMs keep working with a single connection string. A router has a full Postgres query parser, a distributed query planner, query buffering and more. It parses your query, builds a plan that decides which shards should run it, sends the work out, and combines the results back into one stream. Routers can scale vertically and horizontally, so no single router becomes the bottleneck.</p><h3 id="sharding-and-shard-groups"><a href="https://planetscale.com/blog/introducing-neki#sharding-and-shard-groups">Sharding and shard groups</a></h3><p>Every shard in Neki is real Postgres with 1 primary and at least 2 replicas, spread across availability zones. There is no modified storage engine. Extensions, SQL support, and performance behave the way Postgres behaves, because it <em>is</em> Postgres.</p><p>Shards are organized into shard groups, so different tables or workloads can live on different sets of shards. Each shard uses a configuration profile that defines its instance size, replica count, storage, Postgres parameters, and extensions, so you can size each group for its own traffic.</p><h3 id="connection-pooling"><a href="https://planetscale.com/blog/introducing-neki#connection-pooling">Connection pooling</a></h3><p>Sidecars run alongside every Postgres instance. This is the piece that makes Neki&#x27;s connection handling meaningfully better than just sticking PgBouncer in front of a database. Because Neki controls both ends of the connection, the router side and the Postgres side, it can size pools to what each instance can actually serve instead of estimating from outside the process.</p><h3 id="control-plane"><a href="https://planetscale.com/blog/introducing-neki#control-plane">Control plane</a></h3><p>The control plane tracks the health of every node, runs planned switchovers and unplanned failovers, and coordinates the workflows that reshard data, apply schema changes, and perform version upgrades.</p><h3 id="data-topology"><a href="https://planetscale.com/blog/introducing-neki#data-topology">Data topology</a></h3><p>Tying it together is the <a href="https://planetscale.com/blog/what-is-a-data-topology">data topology</a>, a JSON configuration that maps your logical tables onto physical shards. You define shard indexes, which specify the column Neki routes on and how that value gets hashed, and shard groups, which control how many shards a set of tables spreads across and which shards those are. Routers cache the topology and consult it on every plan.</p><h2 id="what-you-get-beyond-sharding"><a href="https://planetscale.com/blog/introducing-neki#what-you-get-beyond-sharding">What you get beyond sharding</a></h2><p>Everything you would normally schedule a maintenance window for runs as a built-in workflow in Neki. Workflows provision new target nodes, catch them up with replication, switch traffic with a <code>__neki</code> metafunction, and retire the old nodes. All through the same <code>psql</code> connection your application uses.</p><p>This online operations model covers schema changes, version upgrades, planned and unplanned failovers, imports, and resharding.</p><p>Neki also includes all of the features you’ve come to rely on with PlanetScale: Insights, schema recommendations, branching, MCP, and more.</p><p>You can also run Neki unsharded, as a single primary with replicas. You get the improved connection pooling, online DDL, zero downtime upgrades, and health monitoring before you need to shard. When you do, resharding is a workflow you run against the cluster you already have.</p><h2 id="what-is-a-platform-preview"><a href="https://planetscale.com/blog/introducing-neki#what-is-a-platform-preview">What is a platform preview?</a></h2><p>We wanted to get Neki into your hands as soon as possible. You should not run production workloads on Neki during the platform preview. The product is still changing, and some of those changes will be breaking.</p><p>If you have any feedback, questions, or face any issues during the platform preview, please let us know. Fill out a <a href="https://planetscale.com/contact">support ticket</a> or <a href="https://pscale.link/community">join our Discord</a></p><h2 id="try-neki-today"><a href="https://planetscale.com/blog/introducing-neki#try-neki-today">Try Neki today</a></h2><p>Sign in to PlanetScale, opt in to the platform preview, and create a Neki cluster. Read the <a href="https://planetscale.com/docs/neki">Neki docs</a> for more information about Neki&#x27;s architecture, how to shard, and more.</p><p>If you have a large Postgres cluster and are curious whether Neki is a good fit, <a href="https://planetscale.com/contact">get in touch</a>. We would love to do a private demo for your team, dig into your schema and query patterns, and give you real suggestions on how to shard.</p>]]></content>
    <summary><![CDATA[Neki, sharded Postgres by PlanetScale, is now available in platform preview.]]></summary>
  </entry>
  <entry>
    <title>How one connection kills a database</title>
    <link href="https://planetscale.com/blog/debugging-live-database-connections"/>
    <id>https://planetscale.com/blog/debugging-live-database-connections</id>
    <published>2026-08-31T00:00:00.000Z</published>
    <updated>2026-08-31T00:00:00.000Z</updated>
    <author>
      <name>Simeon Griggs</name>
    </author>
    <category term="product"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<p>Think you could land a DBA job at GitHub?</p><blockquote><p><em>You&#x27;ve just been paged. Every query is failing to execute. Find out why.</em></p></blockquote><p>Years ago at GitHub, this was a question <a href="https://x.com/samlambert">Sam Lambert</a> would ask in interviews.</p><p>The solution involved a downed database that started with a MySQL schema change needing an exclusive lock on a table, but it couldn&#x27;t get one. Another session had already touched that table and never committed. That open transaction blocked the schema change.</p><p>Once the schema change was waiting, new queries on the same table queued up behind it. Nothing was deadlocked. Nothing would time out on its own. The whole pile-up sat there until someone found the connection at the top of the chain and killed it.</p><p>(By the way, you&#x27;d avoid this MySQL lock on PlanetScale Vitess since <code>ALTER</code> would have been an online schema change, copying in the background and only taking a brief lock at cutover.)</p><p>This isn&#x27;t rare, and it isn&#x27;t exclusive to MySQL. You can reproduce the same pile-up on Postgres just as easily.</p><h2 id="want-to-see-a-stuck-database"><a href="https://planetscale.com/blog/debugging-live-database-connections#want-to-see-a-stuck-database">Want to see a stuck database?</a></h2><p>You won&#x27;t need elevated permissions. A normal application connection is enough.</p><p>All it takes is an unhandled exception and a poorly timed migration.</p><iframe src="https://planetscale.com/blog/debugging-live-database-connections/iframe#lock-queue" title="Interactive: a finished SELECT stays idle, a migration waits on it, and later queries queue behind the migration" loading="lazy"></iframe><p>Say you have a Postgres database with a hot table named <code>orders</code>.</p><p>On one <strong>connection</strong>, a <strong>transaction</strong> begins a <code>SELECT</code> <strong>query</strong>, but the app throws an exception.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- Connection A: Ordinary application user</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">BEGIN</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SELECT</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> *</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> FROM</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> orders </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">WHERE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">=</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 123</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- ...then the app throws an exception 💀</span></span>
<span class="line"></span></code></pre></div></div><p>The <code>SELECT</code> <strong>query</strong> finished, but Postgres keeps holding the <strong>connection</strong>, waiting for the <strong>transaction</strong> to end with <code>COMMIT</code> or <code>ROLLBACK</code>, which it never does because of the app&#x27;s thrown exception.</p><p>While Postgres waits on that connection, another connection attempts to run a migration.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- Connection B: Migration role waiting on lock</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">ALTER</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TABLE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> orders </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">ADD</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> COLUMN foo </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">integer</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"></span></code></pre></div></div><p>The <code>SELECT</code> query only needed a read lock (<code>ACCESS SHARE</code>). The migration needs a lock that blocks all other access (<code>ACCESS EXCLUSIVE</code>), so it waits.</p><p>Postgres will not let later queries jump the queue. Every new query on <code>orders</code> waits behind the already-stuck migration, which is itself stuck behind the initial <code>SELECT</code> connection that never closed.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- Connections C, D, E, etc: Waiting on the lock</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SELECT</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> *</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">FROM</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> orders</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">WHERE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> customer_id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">=</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> ?;</span></span>
<span class="line"></span></code></pre></div></div><p>If the app doesn&#x27;t handle errors gracefully and depends on quick responses from <code>orders</code>, you&#x27;ve got downtime.</p><p>Not because of a hack, but because one unhandled exception prevented a <code>COMMIT</code>, kept a connection open, blocked a migration, and that migration blocked everything else.</p><p>By the way, this is preventable. It happens because the default for <code>idle_in_transaction_session_timeout</code> is disabled. Give it a value and Postgres times out transactions that haven&#x27;t closed.</p><h2 id="freeing-a-stuck-database"><a href="https://planetscale.com/blog/debugging-live-database-connections#freeing-a-stuck-database">Freeing a stuck database</a></h2><p>Debugging the state of connections in MySQL and Postgres can be clunky.</p><p>As covered in <a href="https://planetscale.com/blog/see-what-your-database-is-doing-right-now">See what your database is doing right now with Connections </a> it is possible to run a query on a loop in your terminal to see active connections. But it&#x27;s not an ideal interface.</p><p>It also depends on your database having spare connection slots. The worst-case scenario is your database is overwhelmed with connections that haven&#x27;t been closed, meaning even you can&#x27;t connect to debug and kill them.</p><p>Because this experience is so terrible, we&#x27;ve added the ability to view and kill connections in Postgres and MySQL databases on PlanetScale.</p><h2 id="connections-in-the-dashboard"><a href="https://planetscale.com/blog/debugging-live-database-connections#connections-in-the-dashboard">Connections in the dashboard</a></h2><p>PlanetScale&#x27;s connections tooling uses a reserved administrative connection so it can still show you a list of active connections and processes even when connections are exhausted and your application can&#x27;t connect.</p><p>In the PlanetScale dashboard, you can now click the Connections tab to view a live list of connections. You&#x27;ll see a bunch of useful columns like Process ID, State, and Duration, but most importantly, you will see a column for Blocked Queries.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-list-D9J8Ki6H.png?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-list-darkmode-DqWq7MBR.png?auto=compress%2Cformat"><img alt="The PlanetScale dashboard Connections list, with process ID, state, blocked queries, and query columns" src="https://planetscale-images.imgix.net/assets/connections-pg-list-D9J8Ki6H.png?auto=compress%2Cformat" width="2448" height="1346" loading="lazy"></picture></p><p>From here, it&#x27;s easy to see which connections are currently blocking others from proceeding. You can click into any connection to see more details, such as the query currently being performed by that connection.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-detail-B3YhOhsL.png?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-detail-darkmode-DDc7McTr.png?auto=compress%2Cformat"><img alt="The Connections detail panel for a selected process, with wait event, blocked queries, and a Kill process action" src="https://planetscale-images.imgix.net/assets/connections-pg-detail-B3YhOhsL.png?auto=compress%2Cformat" width="2448" height="1346" loading="lazy"></picture></p><p><strong>Kill process</strong> gives you three options, from least to most disruptive:</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-kill-BBc_B223.png?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/connections-pg-kill-darkmode-CCIR1PwN.png?auto=compress%2Cformat"><img alt="The Kill process modal, with Cancel query, Terminate transaction, and Terminate connection" src="https://planetscale-images.imgix.net/assets/connections-pg-kill-BBc_B223.png?auto=compress%2Cformat" width="2448" height="1346" loading="lazy"></picture></p><ul><li><strong>Cancel query</strong> stops the running statement and leaves the connection open, so the app can send another query</li><li><strong>Terminate transaction</strong> rolls back the open transaction and closes the connection, but only if that same transaction is still running</li><li><strong>Terminate connection</strong> drops the backend entirely, whether it is idle or mid-query</li></ul><p>Thinking back to the downtime example, the idle <code>SELECT</code> had already finished, so canceling the query will not release the lock. In that scenario you would need to terminate the transaction or the connection.</p><p>Over on Vitess, each process has two options: cancel the query or terminate the connection. In a sharded database, you can view the process list per keyspace and shard.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/connections-vitess-list-Bd1fOrzO.png?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/connections-vitess-list-darkmode-ahtFt-ov.png?auto=compress%2Cformat"><img alt="The PlanetScale dashboard Connections list on a Vitess database, scoped to a keyspace and shard" src="https://planetscale-images.imgix.net/assets/connections-vitess-list-Bd1fOrzO.png?auto=compress%2Cformat" width="2448" height="1346" loading="lazy"></picture></p><h2 id="connections-via-cli"><a href="https://planetscale.com/blog/debugging-live-database-connections#connections-via-cli">Connections via CLI</a></h2><p>If you&#x27;d rather work with a terminal UI, launch:</p><div class="code-block"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>pscale branch connections top &#x3C;database> &#x3C;branch></span></span>
<span class="line"><span></span></span></code></pre></div></div><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/live-connections-top-DLKSy3CO.png?auto=compress%2Cformat"><img alt="The pscale branch connections top view, with a stuck checkout transaction at the top" src="https://planetscale-images.imgix.net/assets/live-connections-top-DLKSy3CO.png?auto=compress%2Cformat" width="2333" height="1409" loading="lazy"></picture></p><p>This opens an interactive live view that refreshes about once every second. The most important connections are sorted toward the top. You can navigate through the view using keyboard shortcuts and open any connection to see more details.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/live-connections-blockers-Dc1L2c1b.png?auto=compress%2Cformat"><img alt="The blocker tree: one idle checkout-api transaction holding up the refund, payment, and cancel updates queued behind it" src="https://planetscale-images.imgix.net/assets/live-connections-blockers-Dc1L2c1b.png?auto=compress%2Cformat" width="2355" height="1418" loading="lazy"></picture></p><p>If your agents prefer the current state of connections in JSON, use this one-shot command instead of the live view:</p><div class="code-block"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>pscale branch connections show &#x3C;database> &#x3C;branch> --format json</span></span>
<span class="line"><span></span></span></code></pre></div></div><p>That output includes the IDs needed to cancel a query or terminate a connection, which you can still require confirmation for. See the <a href="https://planetscale.com/docs/cli/connections">CLI reference</a> for the kill commands that go with it.</p><h2 id="migrate-today-stress-less-later"><a href="https://planetscale.com/blog/debugging-live-database-connections#migrate-today-stress-less-later">Migrate today, stress less later</a></h2><p>The correct answer to land your dream job today is &quot;Use PlanetScale.&quot;</p><p>Next time you&#x27;re paged because everything is erroring, <a href="https://planetscale.com/migrate">I hope you&#x27;ve already migrated over</a>. The interview answer hasn&#x27;t changed, but connection management on PlanetScale has made it easier than ever before.</p>]]></content>
    <summary><![CDATA[Unblock stuck connections with live connection management in the PlanetScale CLI and dashboard]]></summary>
  </entry>
  <entry>
    <title>Problems with large tables in Postgres</title>
    <link href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres"/>
    <id>https://planetscale.com/blog/dealing-with-large-tables-in-postgres</id>
    <published>2026-08-25T00:00:00.000Z</published>
    <updated>2026-08-25T00:00:00.000Z</updated>
    <author>
      <name>Simeon Griggs</name>
    </author>
    <category term="engineering"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<p>Tell me to stop when I name the largest table in your database: <code>logs</code>, <code>ledger</code>, <code>notifications</code>, <code>feed</code>, <code>events</code>, <code>chonk</code> ... admit it, I got it, right?</p><p>Product success leads to lots of data, lots of data leads to big tables, but big tables lead to predictable problems.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#tables" title="Interactive: table sizes in an imagined database" loading="lazy"></iframe><p>A table can be large (many rows), wide (many columns), or &quot;fat&quot; (oversized values). Any of these can cause you problems.</p><h2 id="how-a-large-table-can-cause-an-outage"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#how-a-large-table-can-cause-an-outage">How a large table can cause an outage</a></h2><p>First, let me tell you a story.</p><p>A customer had two tables, A and B. Table B is large and unpartitioned. It has a foreign key to A with cascade delete turned on. A single delete on A has to delete every child row in B. That can be 100s or 1000s of rows in B for every one in A.</p><p>In this customer&#x27;s case, deletions from A ran in a background job and attempted to remove around 100,000 rows from A. The deletes took so long that they timed out. The job retried. Each attempt generated a pile of WAL (the write-ahead log Postgres uses to record every change), which saturated the network and CPU between the primary and its replicas. This caused the replicas to lag.</p><p>The application checked replica LSN (a replica&#x27;s position in that WAL stream) before sending a read to a secondary. The secondaries were not caught up, so every query went to the primary.</p><p>The primary was already busy writing WAL. It also had to respond to all of the reads. And so a simple cascade delete on a large table quickly became an outage.</p><h2 id="possible-fixes-for-a-large-table"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#possible-fixes-for-a-large-table">Possible fixes for a large table</a></h2><p>The problems covered in this post are common to all Postgres databases but are made worse by those containing large tables. You might reach for one of a few solutions, which we&#x27;ll examine throughout.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> splits a large table based on a key value. For example, if your large table stores rows with a specific date, such as when an event occurred, cutting that table into partitions of months or years creates more, smaller, and more manageable tables.</p><p>This can be useful for some large-table problems.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> refers to adding more resources to make more space or brute-forcing your way out of performance bottlenecks such as increasing CPU, RAM, or buying larger and faster disks.</p><p>It&#x27;s almost always a band-aid solution that only hides the real problem.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding (horizontal scaling)</span> splits the large table, or the entire database, into separate database clusters connected by a router to act as one. Just as Vitess is sharded MySQL, <a href="https://planetscale.com/neki">Neki</a> is sharded Postgres.</p><p>With more, smaller, and isolated database clusters, almost all of your expected large table problems are resolved. More on that later in the post.</p><h2 id="slow-late-vacuum"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#slow-late-vacuum">Slow, late vacuum</a></h2><p>You may already know that deleted rows in Postgres aren&#x27;t automatically removed; they are <em>marked</em> for deletion. These marked rows hold space until the vacuum process reclaims it. Read <a href="https://planetscale.com/blog/postgresql-mvcc">Every UPDATE leaves a ghost</a> to learn more.</p><p>What you might not realize is that vacuum runs <strong>per table</strong>.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#vacuum" title="Interactive: small tables vacuum many times while one large table is still running" loading="lazy"></iframe><p>Vacuuming a single large table can tie up a worker for a long time. Vacuum uses a ring buffer so it does not evict the rest of <code>shared_buffers</code> (Postgres&#x27;s shared page cache), but on a table larger than RAM it still competes with user queries for the same disks.</p><p>Large tables take longer to vacuum, and they also wait longer to start.</p><p>On Postgres 17, the default trigger is 20% of the table. A table with ~500 million rows needs ~100 million dead tuples before autovacuum begins. You can modify this value cluster-wide or per table by changing <code>autovacuum_vacuum_scale_factor</code>.</p><p>Postgres 18 added <code>autovacuum_vacuum_max_threshold</code> with a default value of 100,000,000. So a billion-row table triggered autovacuum at 100 million (still a high number) instead of 200 million. Below 500 million rows, the old scale factor was still in play.</p><p><a href="https://planetscale.com/blog/whats-new-in-postgres-19">Postgres 19 adds parallel autovacuum</a>, <code>off</code> by default. Extra workers can finish one large table faster, but that still doesn&#x27;t change when vacuum starts.</p><p>Autovacuum also runs <code>ANALYZE</code>, which has the same default-doesn&#x27;t-scale problem. It samples a fixed number of rows no matter how large the table is. So the planner&#x27;s estimates potentially get worse as the table grows. Raise the target with <code>ALTER TABLE ... SET STATISTICS</code> on the columns the planner actually needs to get right.</p><p>Vacuum on large tables is late to start, slow to finish, with more dead rows while it runs. Each cycle is worse than the last, even when vacuum reports success.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> can be a good first move to split your large table into more reasonable-size chunks, if it has a suitable key.</p><p>Partitioning also helps avoid autovacuum&#x27;s late start. Each partition is its own smaller heap, so the 20% trigger fires after far fewer dead tuples. Workers vacuum partitions in parallel and <code>ANALYZE</code> samples a smaller relation.</p><div class="table-overflow"><table><thead><tr><th>Shape</th><th>Rows the formula sees</th><th>Default trigger (50 + 0.2 × rows)</th></tr></thead><tbody><tr><td>One heap, 500 million rows</td><td>500,000,000</td><td>~100,000,050 dead tuples</td></tr><tr><td>Same data as 12 monthly partitions</td><td>~41.7 million each</td><td>~8.3 million dead tuples per partition</td></tr></tbody></table></div><p>Disks and <code>shared_buffers</code> are still shared with the rest of the cluster, so it&#x27;s not a perfect solution.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> can help more workers finish sooner with faster disks. That does not change the trigger on one huge heap, and the table may still not fit in RAM.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding (horizontal scaling)</span> gives you the best of both worlds. Smaller tables on smaller databases, each with their own isolated resources.</p><p>Sharding also solves foundational parts of Postgres that can&#x27;t be resolved by more resources.</p><p>Every write transaction gets a 32-bit ID, and vacuum uses these IDs (<code>xmin</code>) to decide which row versions are still visible. These IDs are cluster-wide, not per-table. Vacuum freezes old tuples so XIDs can be reused; wraparound is the age of the oldest unfrozen XID (<code>relfrozenxid</code> in <code>pg_class</code>), not how many XIDs the database has ever issued. If that age reaches around 2.1 billion, Postgres goes into read-only mode. <a href="https://planetscale.com/blog/postgres-backups-under-the-hood#logical-backups-and-transaction-wraparound">Postgres backups under the hood</a> covers transaction wraparound in more detail.</p><p>Since each shard is its own cluster, a large table separated across shards is less likely to put a cluster into read-only mode.</p><p>Additionally with isolated I/O, any work vacuum does on one shard does not contend with queries on another. <code>ANALYZE</code> becomes a per-shard operation.</p><h2 id="wasted-time-on-incomplete-repacking"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#wasted-time-on-incomplete-repacking">Wasted time on incomplete repacking</a></h2><p>Vacuum can be prevented from reclaiming space even if it finishes successfully.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#repack" title="Interactive: vacuum needs three passes to reclaim every dead tuple" loading="lazy"></iframe><p>Vacuum can&#x27;t remove a dead tuple while any transaction in the cluster still needs it.</p><p>But a slow vacuum on a large table may have wasted its time to completion. Many of the rows it found to remove may still be required by transactions that started before vacuum finished and haven&#x27;t completed yet. Vacuum will need to walk the entire table again later.</p><p>This can be an issue on tables of any size, but is more annoying on large tables as they take longer to complete.</p><p>Free space that was successfully reclaimed can be reused for new rows, but the table file usually does not shrink. That leftover file size is bloat.</p><p>Should your table get bloated, PlanetScale Insights scans once a day and opens a recommendation when estimated bloat is over 25% and 100MB.</p><p>To compact a bloated table while reads and writes continue, enable <a href="https://planetscale.com/docs/postgres/extensions/pg_squeeze"><code>pg_squeeze</code></a> on the database&#x27;s Clusters page (that requires a restart), then run a one-time squeeze or register the table for regular cleanup.</p><p>Be aware that <code>pg_squeeze</code> has no throttle. Squeezing a large table can saturate I/O and CPU and cause an outage. Only run it when the cluster has spare capacity.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> creates more, smaller heaps. So a wasted pass is cheaper and can be retried sooner. Squeeze is per partition, so the spare disk you need to run squeeze is the size of a partition, not the whole table.</p><p>Splitting the table does not split the snapshot horizon. <code>xmin</code> is still cluster-wide, so a long query anywhere on the cluster still pins every partition.</p><p>You can drop old or bloated partitions instead of compacting them.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> adds more spare disk space, which may be the only reason squeeze can run at all and/or finish sooner. That lets you compact the same large file faster.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding</span> creates distinct Postgres database clusters, so a long query or dump on one shard doesn&#x27;t pin vacuum on any other. Squeeze runs only on the copy in its shard, not the entire large table. Each cluster tracks its own <code>xmin</code>.</p><p>In a sharded database, wasted vacuum passes are cheaper and less likely to happen.</p><h2 id="slow-queries-hold-connections-longer"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#slow-queries-hold-connections-longer">Slow queries hold connections longer</a></h2><p>It is impossible to write a Postgres article about performance and not mention that <em>Postgres has a connection-per-process architecture</em>. Connection hygiene matters even more when your database has a large table.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#connections" title="Interactive: two connections stuck on slow queries against events" loading="lazy"></iframe><p>Sequential scans (reading large segments of the table from start to finish), big sorts, and heavy joins against a large table take longer, so they hold connections longer.</p><p>Raising <code>max_connections</code> feels like a logical solution to avoid &quot;too many clients already,&quot; but this only lets more concurrent connections perform these slow queries. At best, this starves other workloads of new connections. At worst, a traffic spike on your large table is more likely to trigger an out-of-memory (OOM) event.</p><p>A large table will not stay in <code>shared_buffers</code>. More processes scanning it means more cache eviction, more disk usage, slower queries, and longer-held connections.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> splits your large table, which can help queries finish faster (and release connections sooner) so long as they access only one or a few partitions. But cross-partition queries can get worse.</p><p>Buy all the RAM and CPU you like with <span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">vertical scaling</span>, but you&#x27;re still not putting a hundreds-of-GB table into cache. You cannot spend your way out of inefficient connection handling.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding</span> spreads your backends across multiple clusters, where each shard-distinct query is shorter and more likely cached.</p><p>A connection held by a large table is one the rest of the product cannot use. With workloads spread across shards there may be less overlap.</p><p>A scatter-gather query with no shard key can hold more connections, not fewer. Selecting the right key to shard on is critical to seeing performance benefits from sharding.</p><h2 id="slower-backups-and-recovery"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#slower-backups-and-recovery">Slower backups and recovery</a></h2><p>Postgres gives you three ways to take a backup. A large table complicates each one in a unique way.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#backups" title="Interactive: copying a large table looks busy but barely makes progress" loading="lazy"></iframe><p>A logical dump with <code>pg_dump</code> writes live rows, so dead tuples stay out of the output. The dump still walks the bloated heap, and it holds a transaction snapshot the whole time. On a small table, that pin is brief. On a large table, the backup is a long-running transaction against the cluster.</p><p>A file system backup copies the data directory, bloat and all. Faster than dumping rows. A consistent copy usually means shutting the database down, or relying on atomic snapshots from the file system. You might be okay with a maintenance window for a database of small tables, but a single large table could add hours just to copy one relation.</p><p>Continuous archiving copies the data files while Postgres is still running, then keeps the WAL so you can replay through the modified bits and land on a consistent copy. No dump pin, no shutdown. You still copy the large table, bloat, and its indexes. What grows with the table is restore time. How long an incident waits on this heap plus the WAL written while the copy ran.</p><p>That third option is <a href="https://planetscale.com/blog/postgres-backups-under-the-hood">how PlanetScale backs up Postgres</a>. We copy the data files and continuously archive WAL on a throwaway node rather than on your primary, so the backup doesn&#x27;t compete with production queries for disk and doesn&#x27;t pin the primary.</p><p>We can&#x27;t skip the size of those files. A large table that doesn&#x27;t fit in memory, plus its indexes, plus its bloat, is what has to land in object storage.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> rearranges files but doesn&#x27;t shrink the copy. A pin held during dump is still cluster-wide. A physical backup includes the same bloat even if it is split across smaller tables.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> means faster copies through more resources. But the files aren&#x27;t any smaller, so restore is only quicker because the disks are faster.</p><p>But neither partitioning nor vertical scaling changes the <em>size</em> of the data.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding</span> breaks down backups and restores into smaller, distinct units of work, allowing them to complete much faster. Recovery time depends on the slowest shard instead of the largest table.</p><h2 id="too-many-indexes"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#too-many-indexes">Too many indexes</a></h2><p>A large table isn&#x27;t viable to scan from start to finish and is likely queried in many different ways.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#indexes" title="Interactive: indexes keep accumulating until they push the events table off the page" loading="lazy"></iframe><p>To keep queries fast, you keep adding indexes to the table. While indexes make queries fast, they aren&#x27;t free. They take disk, get vacuumed, get backed up, and make every write touch more files.</p><p>There are no good options here. Prune indexes and your queries get slower. Accumulate indexes and pay for it with an enlarged heap, while also vacuuming, packing, and copying many access paths.</p><p>The indexes that make a large table usable are part of what makes it too large to keep indexing.</p><p>Indexes on a <span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">partitioned</span> table are created on the logical table but stored and updated on each partition. Those smaller indexes are faster to build and cheaper to <code>REINDEX</code>.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> buys you more space to store more indexes ... but that&#x27;s like fixing traffic congestion by adding another lane to the freeway.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding</span> creates more, smaller databases, so having more indexes is less of a problem. You can afford to create many more indexes than if your large table was on a single cluster since a write only updates indexes on the shard that owns that row. Sharding is the <em>good option</em> for balancing a lot of data and many indexes.</p><h2 id="wide-tables-split-across-pages-and-files"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#wide-tables-split-across-pages-and-files">Wide tables split across pages and files</a></h2><p>A table is not only large because it has many rows; it might be that the rows are wide with many columns, or each row contains a large amount of data.</p><iframe src="https://planetscale.com/blog/dealing-with-large-tables-in-postgres/iframe#wide" title="Interactive: a wide row fills most of a page so the next row has to take a new one" loading="lazy"></iframe><p>If a row exceeds ~2KB, oversized values go to TOAST, a separate table and indexes next to the heap you are already struggling to vacuum.</p><p>Each toasted value gets a 32-bit OID, about 4 billion per table. Updates of toasted values take a new OID. High-churn wide tables fill that space, and inserts slow down as Postgres hunts for a free one before the hard stop.</p><p>Whatever is left still has to fit into a single 8KB page, as Postgres will not split a row across pages. If a tuple cannot fit in the free space on any existing page, it gets a new page.</p><p>For a read-heavy table, you want pages packed tight (a high fillfactor), so scans walk as few pages as possible. For an update-heavy table, you want the opposite. Lower the fillfactor so an update can often stay on the same page instead of allocating a new one.</p><p>On a small table, the wrong fillfactor is a few wasted pages. On a large, wide table it could be terabytes of sparse pages, or a constant stream of new ones that vacuum, WAL, and backups all have to follow.</p><p>A large table is a heap, plus indexes, plus TOAST, and some operations still walk those files one at a time. <code>pg_database_size()</code> is one example. It stats every file in the database serially. On a large, wide table, that can pin a core.</p><p><span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Partitioning</span> is a genuinely good solution for this class of large-table problems. Fillfactor can differ per-partition. Each partition gets its own TOAST table, so OID space is per slice.</p><p>However, with more files in one cluster, <code>pg_database_size()</code> can get worse.</p><p><span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">Vertical scaling</span> can hide a bad fillfactor for a while with extra disk space. Faster disks make TOAST vacuum and file <code>stat()</code>s cheaper. But you cannot buy greater limits than 8KB pages, the 2KB TOAST threshold, or the 4 billion OIDs.</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">Sharding</span> creates individual Postgres instances, so TOAST OID space resets per cluster. A bad fillfactor hurts less because the heap is smaller. File walks are per shard.</p><p>Sharding does not, by itself, let hot and cold rows use different fillfactors. Partitioning still wins there.</p><h2 id="sharding-is-the-solution-to-large-tables"><a href="https://planetscale.com/blog/dealing-with-large-tables-in-postgres#sharding-is-the-solution-to-large-tables">Sharding is the solution to large tables</a></h2><p>Just as partitioning breaks up a single table into many small pieces, sharding distributes the workload of your reads and writes across many database clusters.</p><p>Let&#x27;s revisit the cascade delete story.</p><p>When replicas lagged, every read went to the primary. On a sharded database, each shard is its own cluster, with its own WAL and replicas. If the affected rows of A and B lived on one shard, that shard could still lag, and its reads pile onto its primary, but that could not fail the LSN check for the rest of the product. If those 100,000 deletes were spread across shards, the WAL burst would be split. No replica stream would have to replay the whole thing.</p><p>Your single-cluster large table likely processes a high throughput of traffic. Vertical scaling can help, but sharding does it better. With more clusters receiving and processing writes, a single table doesn&#x27;t strain the entire cluster.</p><p>Additionally, cluster limits that cannot be solved by partitioning or vertical scaling are resolved. Postgres&#x27; hard-coded ceilings of 32-bit XIDs, TOAST OID and more become per-shard limits instead.</p><p>Sharded database backups are individually smaller and can be <a href="https://planetscale.com/blog/massively-parallel-postgres-backups">massively parallelized</a>, making database restoration faster as well. Operations finish at the time of the slowest shard, not all at once waiting on large, bloated tables.</p><p>Cache eviction is a problem with large tables and their working sets. While more resources won&#x27;t fit a large table into RAM, you often can per shard. Shard-local queries become shorter and faster.</p><p>Neki, sharded Postgres, lets your application write queries as if it were a single database, while splitting a large table into smaller, automatically distributed parts.</p><p>It contains the logic for where to send reads and writes. You still have to determine how that data is spread. That fan-out is covered in our post on <a href="https://planetscale.com/blog/what-is-a-data-topology">data topologies</a>.</p><p>Sharding shares the large table load across multiple clusters. If that&#x27;s a problem you need solved, <a href="https://auth.planetscale.com/sign-up">sign up for a PlanetScale account</a> to use Neki today.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>If you&#x27;re interested, we also have an article covering <a href="https://planetscale.com/blog/dealing-with-large-tables">big tables with MySQL and Vitess</a>.</p></div>]]></content>
    <summary><![CDATA[Postgres presents pretty predictable performance problems when dealing with large tables. Sharding solves this.]]></summary>
  </entry>
  <entry>
    <title>The history of Postgres sharding</title>
    <link href="https://planetscale.com/blog/the-history-of-postgres-sharding"/>
    <id>https://planetscale.com/blog/the-history-of-postgres-sharding</id>
    <published>2026-08-24T00:00:00.000Z</published>
    <updated>2026-08-24T00:00:00.000Z</updated>
    <author>
      <name>Josh Brown</name>
    </author>
    <category term="engineering"/>
    <category term="neki"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<p>Over the past 20 years, many companies have achieved such a scale that their database could no longer support the workload assigned to it. The solution is almost universally to shard the database, but only when absolutely necessary due to the added complexity.</p><p>MySQL was a much more popular choice for relational databases in the 2010s and because of this, the tooling around things like online schema changes and sharding progressed much quicker. Tools like <a href="https://github.com/github/gh-ost">gh-ost</a>, <a href="https://planetscale.com/vitess">Vitess</a>, and more were built around this ecosystem.</p><p>Postgres, on the other hand, had one-offs, then Citus, then proxy-like solutions. Now, in 2026, <a href="https://planetscale.com/neki">Neki</a> builds on the lessons learned from the incredible engineers that have built not only Vitess, but all the other sharding solutions that came before it.</p><p>To fully understand Neki, and why we built it, it&#x27;s important to have an understanding of how we got here.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/timeline-B6yj_Ow3.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/timeline-darkmode-C5eUHlHZ.svg?auto=compress%2Cformat"><img alt="A twenty-year timeline from Ultima Online shards through Skype, Vitess, Instagram, Citus, and Neki." src="https://planetscale-images.imgix.net/assets/timeline-B6yj_Ow3.svg?auto=compress%2Cformat" width="1504" height="300" loading="lazy"></picture></p><h2 id="it-started-in-a-video-game"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#it-started-in-a-video-game">It started in a video game</a></h2><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/uo-Co9VDuNu.png?auto=compress%2Cformat"><img alt="An Ultima Online world copied onto independent parallel shards." src="https://planetscale-images.imgix.net/assets/uo-Co9VDuNu.png?auto=compress%2Cformat" width="1280" height="878" loading="lazy"></picture></p><p>One of the more prominent theories is that the term &quot;Shard&quot; originated, of all places, from a video game. Ultima Online is an <a href="https://en.wikipedia.org/wiki/Massively_multiplayer_online_role-playing_game">MMORPG</a> created in 1997. The developers quickly realized they needed to host players on multiple servers to sustain growth.</p><p>To support this, they invented lore that would make the idea of multiple servers make sense in the fictional universe. <a href="https://www.raphkoster.com/2009/01/08/database-sharding-came-from-uo/">After digging through heaps of lore</a>, &quot;Shards&quot; seemed like the best fit.</p><p>At the end of Ultima I, an evil wizard is defeated who had tried to trap the world into a crystal. When the wizard is defeated, his crystal breaks into &quot;shards&quot;, each holding a copy of the world within it.</p><p>And thus, the term &quot;sharding&quot; was used to denote parallel, independently evolving servers. This term slowly made its way through the industry, narrowing its meaning to database sharding in particular.</p><h2 id="why-mysql-got-there-first"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#why-mysql-got-there-first">Why MySQL got there first</a></h2><p>The LAMP stack was all the rage in the early 2010s. This gave the sharding story for MySQL a head start, as most of the successful companies had started in an era where MySQL was the popular choice.</p><p>Facebook was one of the first companies to truly hit a global scale, serving over 100m active users in 2008 on roughly 1,800 MySQL nodes. Initially, sharding was done at the application level. Each user was assigned an ID that could be used to determine which shard their data lives on.</p><p>Eventually, as Facebook grew, their need for a specialized layer on top of MySQL became apparent. Facebook created <a href="https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/">TAO</a> to accommodate the scale and access patterns that fit their needs.</p><p>YouTube, similarly, started as a single primary node. When the load grew too high, they added replicas. When the primary could no longer handle write pressure, they began the monumental task of sharding.</p><p>Originally, their solution was also baked into the application layer itself. YouTube&#x27;s backend would determine which database server a user request should go to. Around 2010, they pulled out the routing layer into a standalone project, and thus <a href="https://vitess.io/docs/23.0/overview/history/">Vitess was born</a>.</p><p>Many companies that began scaling their LAMP-based stacks slowly consolidated on Vitess as their sharding solution.</p><p>It took Postgres much longer to get a similar solution. Many companies that would later hit Postgres scaling limits built in-house solutions. Most of these were never separated into standalone software, often due to how fine tuned they were for each company&#x27;s needs.</p><h2 id="build-it-yourself-then-live-in-it"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#build-it-yourself-then-live-in-it">Build it yourself, then live in it</a></h2><p>Early Postgres sharding was frequently built as custom, in-house solutions. If a single Postgres node couldn&#x27;t handle your scale, start spinning up more nodes, create a router, and a team of engineers to manage it.</p><p>For companies successful enough to need sharding, dedicating a team to solving database scaling was not an impossible ask. If the database can&#x27;t scale up, the application will start failing, and growth stops.</p><h3 id="skype-and-plproxy"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#skype-and-plproxy">Skype and PL/Proxy</a></h3><p>In 2007 <a href="https://plproxy.github.io/">Skype announced PL/Proxy</a> to the Postgres mailing list. The announced version was Skype&#x27;s second iteration (v2) of PL/Proxy that ran as an extension on a Postgres Proxy database.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/pl-proxy-DHPI1ZZe.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/pl-proxy-darkmode-DHtmfdrB.svg?auto=compress%2Cformat"><img alt="PL/Proxy architecture: one central routing database sending queries to downstream shards." src="https://planetscale-images.imgix.net/assets/pl-proxy-DHPI1ZZe.svg?auto=compress%2Cformat" width="1504" height="540" loading="lazy"></picture></p><p>PL/Proxy was one of the earliest sharding solutions, allowing operators to route queries to different shards by defining SQL functions in the proxy database. The downside to this approach was that every operation that would result in a sharded query would need a defined function to route to the proper shard, defined by the user.</p><p>For instance, if you wanted to shard a <code>users</code> table, you would need to create PL/Proxy functions to insert, update, and read. For each new query pattern, you would need to create a new function and call it from the application layer.</p><p>Although this drastically improved scalability by removing the need for the application to keep track of each downstream shard, the application and database operators would need to work in tandem to ensure every query pattern is defined in the proxy and routed correctly.</p><h3 id="instagram-and-logical-shards"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#instagram-and-logical-shards">Instagram and logical shards</a></h3><p>Instagram is another early pioneer of Postgres sharding. In 2012, Instagram&#x27;s ~30 million users could no longer fit onto a single EC2 instance running Postgres.</p><p>The solution for Instagram was to do logical sharding at the application layer. The application would map thousands of logical schemas down to a few physical shards. Any given physical database could have multiple logical schemas (collections of tables) within it.</p><p>When a given schema grew too large, it could be migrated to a new physical database, and the application would re-map that logical schema to the new database address. This was an elegant solution for their use case, since it required low overhead to manage. This system helped Instagram scale for years.</p><p>Instagram had debated going with PL/Proxy, but decided to hand-roll their own solution to keep with a more minimalistic design, specifically not needing to deal with the SQL function overhead, and for reduced network latency. Their application would talk directly to each shard without a proxy.</p><p>The downside to this approach, however, is that cross shard queries must be managed in the application layer, and moving data between databases requires careful coordination between the app&#x27;s mapping of schemas and physical shards. Sharding was also <a href="https://medium.com/instagram-engineering/sharding-ids-at-instagram-1cf5a71e5a5c">limited to their custom integer ID approach</a>, so every table that needed sharding would also need an ID column.</p><h2 id="then-someone-put-it-in-postgres"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#then-someone-put-it-in-postgres">Then someone put it in Postgres</a></h2><p>Citus was the first open-source sharding solution for Postgres that was designed for wider adoption. Designed originally in 2011, Citus started as a fork of Postgres.</p><p>Later on in 2016, Citus was refactored into a pure extension of Postgres, rather than a fork. Citus shards data by defining nodes as either coordinators or workers.</p><p>Every node is a standalone Postgres instance running the Citus extension. The coordinator node holds the routing map, routes queries as needed, and keeps track of all downstream worker nodes. Worker nodes store the sharded data, and execute the queries passed on to them from the coordinator.</p><p>Citus improves on earlier Postgres sharding solutions by keeping the sharding metadata and routing logic within one central node. This adds some undesirable side effects, however. First, the coordinator becomes the bottleneck. Although some queries in Citus 11+ can go through worker nodes, your application either needs to send all requests to the coordinator node, or keep track of multiple connection strings to each worker it wants to distribute load onto.</p><p>Another downside is that shard management and backups are a semi-manual process. Adding a shard with more resources for a noisy tenant, or many small shards for a wide shard space requires substantial manual configuration of not only the database servers themselves, but wiring them up together with Citus. Managing backups is also external to Citus, so operators still need to build the proper infrastructure to accommodate disaster recovery and healing nodes as needed.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/citus-CBpzQboB.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/citus-darkmode-CPP1ktHJ.svg?auto=compress%2Cformat"><img alt="A Citus cluster with a Citus extension wrapping Postgres on a coordinator and worker nodes." src="https://planetscale-images.imgix.net/assets/citus-CBpzQboB.svg?auto=compress%2Cformat" width="1504" height="640" loading="lazy"></picture></p><p>Other similar attempts have been made to bring sharding to Postgres such as PgDog and Aurora Limitless. PgDog is a spiritual successor to PgCat, both of which improve on Citus&#x27;s architecture substantially.</p><p>PgDog operates as a pure proxy layer in front of Postgres clusters. Although PgDog supports sharding, managing schema changes and resharding operations is still a very involved and manual task. Managing backups and disaster recovery of the Postgres nodes is also up to the operator.</p><p>Aurora operates in a similar fashion. Routers front Postgres nodes backed by EBS, with support for sharding.</p><h2 id="hide-the-shard-key"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#hide-the-shard-key">Hide the shard key</a></h2><p>Other solutions to scaling Postgres resulted in not using real Postgres at all, opting for a Postgres-compatible approach. Google published the <a href="https://cloud.google.com/spanner">Spanner</a> research paper in 2012 while using it internally, later releasing it as a public service in 2017.</p><p>Spanner&#x27;s approach hides sharding from the user and operator altogether, opting for automatic distribution of data across nodes. Instead of defining shard keys or indexes, Spanner splits and moves tables automatically as table size grows.</p><p>This is done by storing the data as key-value pairs. These key-value pairs are grouped into &quot;splits&quot; and replicated across the storage layer, using the <a href="https://en.wikipedia.org/wiki/Paxos_(computer_science)">Paxos</a> consensus algorithm to maintain consistency.</p><p><a href="https://www.cockroachlabs.com/">CockroachDB</a> and <a href="https://www.yugabyte.com/">Yugabyte</a> are similar distributed Postgres-compatible database solutions. Note that they are Postgres-compatible, not actual Postgres solutions. Inevitably this results in lower fine-tune control over the cluster compared to the large amount of settings and flags available for actual Postgres clusters.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>Yugabyte and CockroachDB are open source projects. However, Spanner is closed source.</p></div><p>All of these solutions share the same pitfalls. Performance can be disjoint from expected results since the underlying data access layer is not truly Postgres.</p><p>Extension support also becomes limited, along with some Postgres native SQL features and settings.</p><p>Data placement also becomes hard to predict. Related rows can easily drift across multiple storage locations causing drastically increased latency.</p><p>Explicit sharding makes it easy for engineers to understand and plan around cross-shard latency penalties. Automatic sharding blurs these lines, making it impossible to determine when or how a table will shard, and how drastically performance will drop because of it.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/distributed-sql-CEYpq2Bq.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/distributed-sql-darkmode-DMouKgnD.svg?auto=compress%2Cformat"><img alt="An application request fans out through multiple routers to KV store buckets, with related rows landing on different buckets." src="https://planetscale-images.imgix.net/assets/distributed-sql-CEYpq2Bq.svg?auto=compress%2Cformat" width="1504" height="640" loading="lazy"></picture></p><h2 id="building-the-best-solution"><a href="https://planetscale.com/blog/the-history-of-postgres-sharding#building-the-best-solution">Building the best solution</a></h2><p>The amount of engineering work that went into many of these systems cannot be overstated. As we have seen however, that does not make them perfect. Neki builds on the lessons and foundations from all of its predecessors.</p><p>Neki&#x27;s goal is to raise the bar for scaling Postgres.</p><p>While Spanner and CockroachDB shard data automatically, Neki requires explicit sharding. Developers have complete control over exactly how data is distributed across clusters.</p><p>Neki also uses true Postgres clusters under the hood. This means support for Postgres extensions, stronger SQL compatibility, and predictable performance.</p><p>Compared to PL/Proxy or Citus, sharding is defined by a routing file called the <a href="https://planetscale.com/blog/what-is-a-data-topology">data topology</a>, allowing database operators to define and evolve sharding schemes over time. Since each Neki router only holds a cache of the topology, routers can be horizontally scaled with ease.</p><p>One connection string can go to any router, letting your application treat 1,000 shards as <a href="https://planetscale.com/blog/making-768-servers-look-like-1">one unified database</a>. No single router becomes the bottleneck.</p><p>Each Neki shard has its own primary and replicas. If a primary dies, Neki can bring the cluster back to a healthy state. Routers sit in front, connecting all the shards together.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-CDJ4xt0w.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-darkmode-DOytGnEo.svg?auto=compress%2Cformat"><img alt="An application connects to a Neki router that uses an explicit data topology to send queries to the correct Postgres shards." src="https://planetscale-images.imgix.net/assets/neki-CDJ4xt0w.svg?auto=compress%2Cformat" width="1504" height="705" loading="lazy"></picture></p><p>Neki also allows each group of shards to be independently sizeable and scalable, making it possible to precisely control resources for every part of your cluster.</p><p>Unlike PgDog or Citus, Neki handles backups, restores, node health, <a href="https://planetscale.com/docs/neki/monitoring/query-insights">Insights</a>, connection pooling, schema changes, and much more. By being more than just an extension or pure proxy, Neki can manage every part of the database life cycle.</p><p>Sharding used to be one of the scariest things a growing team could face. Neki aims to change that, making it easier than ever for teams to reach truly planet-scale with as little friction as possible, the familiarity of real Postgres, and incredible performance. <a href="https://auth.planetscale.com/sign-up">Sign up for a PlanetScale account</a> to use Neki today.</p>]]></content>
    <summary><![CDATA[Why has it taken so long to get good Postgres sharding? The last 20 years are the answer.]]></summary>
  </entry>
  <entry>
    <title>What is a data topology?</title>
    <link href="https://planetscale.com/blog/what-is-a-data-topology"/>
    <id>https://planetscale.com/blog/what-is-a-data-topology</id>
    <published>2026-08-17T00:00:00.000Z</published>
    <updated>2026-08-17T00:00:00.000Z</updated>
    <author>
      <name>Harshit Gangal</name>
    </author>
    <author>
      <name>Ahmed Darwich</name>
    </author>
    <category term="engineering"/>
    <category term="neki"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<p>“Vitess for PostgreSQL” is a useful shorthand for the ambition behind Neki, but it significantly understates the architecture we have built. Neki carries forward what we learned from Vitess in a new sharding architecture designed around PostgreSQL’s internals and semantics from the start.</p><p>PlanetScale has spent years building and operating Vitess at scale. That experience has shaped how we approach query routing, data placement, and the challenge of making many database servers behave like one.</p><p>We have previously written about <a href="https://planetscale.com/blog/making-768-servers-look-like-1">how Neki makes hundreds of database servers behave like one</a>. Here, we take a closer look at the mechanism that controls where data lives and how queries are routed.</p><p>One of Neki’s core design goals is flexibility in data placement. There are many ways engineers need to map their logical database schemas onto physical shards in order to optimize for performance and scalability.</p><p>That configuration is expressed as a <strong>data topology</strong>.</p><h2 id="what-is-a-data-topology"><a href="https://planetscale.com/blog/what-is-a-data-topology#what-is-a-data-topology">What is a data topology?</a></h2><p><a href="https://planetscale.com/blog/what-is-database-sharding-and-how-does-it-work">Sharding</a> spreads data across multiple servers, but it also creates a unique routing problem. How does each query find the shard or shards that hold the data it needs?</p><p>Without a routing layer, the application would need to keep track of where its data lives and choose the correct shard for each query.</p><p>A data topology is how Neki answers that question. It is a JSON configuration that describes a PostgreSQL sharding scheme by mapping logical PostgreSQL tables to groups of physical shards and giving routers the information they need to route queries.</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-data-topology-shards-DNE6dqRA.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/neki-router-data-topology-shards-darkmode-DQHVp1U6.svg?auto=compress%2Cformat"><img alt="A customer query enters a Neki router, which uses its cached data topology to select a PostgreSQL shard containing a primary and replicas." src="https://planetscale-images.imgix.net/assets/neki-router-data-topology-shards-DNE6dqRA.svg?auto=compress%2Cformat" width="1504" height="870" loading="lazy"></picture></p><p>A data topology has three building blocks:</p><ul><li><strong>Shard indexes</strong> specify the column or expression Neki uses to route a row and how its value is transformed for routing. The selected column or expression is called the <em>shard key</em>, and its transformed value is the <em>routing key</em> that Neki uses to select a shard.</li><li><strong>Shard groups</strong> define a set of physical shards and assign a range of routing keys to each one. They also specify the default shard index for tables in the group. Related tables that use the same shard key can be colocated, allowing joins and transactions between them to stay local.</li><li><strong>Databases</strong> bind tables to shard groups, following the PostgreSQL hierarchy of databases, schemas, and tables. A table can name its shard group directly or inherit a default from its schema, its database, or the cluster.</li></ul><p>Together, these building blocks tell Neki how to route queries.</p><p>The data topology does not provision or manage physical shards. Those shards are provisioned separately, and the topology refers to each one by a stable ID. In Neki, a shard consists of a PostgreSQL primary and its replicas.</p><h2 id="example-of-a-data-topology"><a href="https://planetscale.com/blog/what-is-a-data-topology#example-of-a-data-topology">Example of a data topology</a></h2><p>Consider a <code>store</code> database that partitions related data around its customers.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">CREATE</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TABLE</span><span style="--shiki-light:#5E49AF;--shiki-dark:#B7A5FB"> customers</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> (</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">  customer_id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">bigint</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> PRIMARY KEY</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">  name</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> text</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">  address</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> text</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">);</span></span>
<span class="line"></span></code></pre></div></div><p>Suppose the <code>customers</code> table needs to be distributed across two shards. Here&#x27;s how a data topology might look:</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/data-topology-json-CCh5Wtoi.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/data-topology-json-darkmode-CSBdTDxQ.svg?auto=compress%2Cformat"><img alt="An example Neki data topology annotated with its shard index, shard group, and database mapping." src="https://planetscale-images.imgix.net/assets/data-topology-json-CCh5Wtoi.svg?auto=compress%2Cformat" width="1504" height="1477" loading="lazy"></picture></p><h3 id="shard-indexes"><a href="https://planetscale.com/blog/what-is-a-data-topology#shard-indexes">Shard indexes</a></h3><p>The topology names this shard index <code>customer_id_xxhash</code>. It tells Neki to convert each <code>customer_id</code> into a routing key using <code>xxhash</code>.</p><p>Neki supports three shard-index strategies:</p><ul><li><strong>Hash-based</strong> indexes use a hash function to map a value into the routing space. Neki uses <code>xxhash</code>.</li><li><strong>Modulo-based</strong> indexes divide an integer by a configured modulus and use the remainder.</li><li><strong>Range-based</strong> indexes use an integer value directly, without hashing it.</li></ul><p>Each strategy takes a different path from <code>customer_id</code> to a physical shard:</p><p><picture class="block"><source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcSet="https://planetscale-images.imgix.net/assets/shard-index-routing-B0_p97UG.svg?auto=compress%2Cformat"><source media="(prefers-color-scheme: dark)" srcSet="https://planetscale-images.imgix.net/assets/shard-index-routing-darkmode-As3uSSoH.svg?auto=compress%2Cformat"><img alt="Three Neki shard-index strategies showing how hash-based routing with xxhash, modulo-based routing, and range-based routing map customer IDs to shards." src="https://planetscale-images.imgix.net/assets/shard-index-routing-B0_p97UG.svg?auto=compress%2Cformat" width="1504" height="958" loading="lazy"></picture></p><p>In this example, <code>customer_id</code> is the shard key, and its value is <code>42</code>. Each strategy transforms its value into a different routing key. <code>xxhash</code> produces <code>0x2e4fe982b68910ac</code>, which falls in the range assigned to <code>customer-shard-1</code>. <code>modulo</code> calculates <code>42 mod 4</code>, producing <code>2</code>, which falls in the range assigned to <code>customer-shard-2</code>. <code>range</code> uses <code>42</code> directly, which falls in the range assigned to <code>customer-shard-1</code>. In each case, the shard group selects the physical shard whose range contains the routing key.</p><h3 id="shard-groups"><a href="https://planetscale.com/blog/what-is-a-data-topology#shard-groups">Shard groups</a></h3><p>The <code>customer_data</code> shard group divides the possible <code>xxhash</code> results between two shards. This example focuses on hash-based routing, but shard groups work similarly with modulo-based and range-based strategies. Key-range boundaries are written as hexadecimal prefixes, so <code>80</code> expands to <code>0x8000000000000000</code>, the midpoint of the routing space:</p><div class="code-block" data-language="text"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>customer-shard-1: 0x0000000000000000 through 0x7fffffffffffffff</span></span>
<span class="line"><span>customer-shard-2: 0x8000000000000000 through 0xffffffffffffffff</span></span>
<span class="line"><span></span></span></code></pre></div></div><p>For example, these two customer IDs land on different shards because their hashes fall in different ranges.</p><div class="code-block" data-language="text"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>customer_id 42 → 0x2e4fe982b68910ac → customer-shard-1</span></span>
<span class="line"><span>customer_id 1  → 0xf36b4a1a44f78bf3 → customer-shard-2</span></span>
<span class="line"><span></span></span></code></pre></div></div><p>Neki first hashes the raw customer ID, then finds the range containing the resulting routing key.</p><p>Every data topology also names an <code>authoritative</code> group, a single-shard home for database-wide metadata such as sequences and schema information. Queries against <code>customers</code> still use <code>customer_data</code>.</p><h3 id="databases"><a href="https://planetscale.com/blog/what-is-a-data-topology#databases">Databases</a></h3><p>The <code>databases</code> section follows the familiar PostgreSQL hierarchy of databases, schemas, and tables.</p><p>Most tables do not need to appear in the data topology. An unlisted table inherits the default shard group from its schema, then its database, then the cluster, allowing a topology to shard an entire database with one default and list only the exceptions.</p><p>Put together, the explicit entry for <code>store.public.customers</code> maps it to <code>customer_data</code>, where it uses the <code>customer_id_xxhash</code> shard index and the group’s key ranges.</p><h2 id="the-foundation-of-neki-internals"><a href="https://planetscale.com/blog/what-is-a-data-topology#the-foundation-of-neki-internals">The foundation of Neki internals</a></h2><p>On one side of a Neki router is the user&#x27;s application, speaking the PostgreSQL wire protocol. On the other, the application&#x27;s data can be spread across an ever-growing number of physical shards.</p><p>The data topology connects the two, informing the router how data is divided and which shard or shards should receive a query. The result is a live configuration that maps logical databases to physical locations. Neki updates it during resharding, table moves, and imports, all while the application continues serving traffic.</p><p>From here, we can explore the rest of Neki’s architecture and how it helps <a href="https://planetscale.com/blog/making-768-servers-look-like-1">make 768 servers look like 1</a>.</p><p>Running PostgreSQL at scale? <a href="https://auth.planetscale.com/sign-up">Sign up for a PlanetScale account</a> to use Neki today.</p>]]></content>
    <summary><![CDATA[A data topology describes the sharding scheme a Neki router uses to map logical PostgreSQL tables to physical shards and route queries.]]></summary>
  </entry>
  <entry>
    <title>Concurrency vs. Throughput: why more parallelism can make databases slower</title>
    <link href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql"/>
    <id>https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql</id>
    <published>2026-08-07T00:00:00.000Z</published>
    <updated>2026-08-07T00:00:00.000Z</updated>
    <author>
      <name>Liz van Dijk</name>
    </author>
    <category term="engineering"/>
    <category term="vitess"/>
    <content type="html"><![CDATA[<p>Not long ago we watched a production MySQL database melt down for sixteen minutes.</p><p>The errors started as a trickle, a handful per minute, then fed on themselves:</p><div class="code-block"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>minute   errors/min   queries/s</span></span>
<span class="line"><span>  0          5          15,000   &#x3C;- a burst of work arrives</span></span>
<span class="line"><span>  2         60           3,900</span></span>
<span class="line"><span>  4        300           2,500</span></span>
<span class="line"><span>  6        550           2,000</span></span>
<span class="line"><span>  8        900           1,900</span></span>
<span class="line"><span> 10      1,400           1,500   &#x3C;- error peak = throughput trough</span></span>
<span class="line"><span> 12        700           1,700</span></span>
<span class="line"><span> 14        250           2,000</span></span>
<span class="line"><span> 16          0           2,500   &#x3C;- locks released, backlog drained</span></span>
<span class="line"><span> 18          0           8,500   &#x3C;- full recovery, throughput jumps 5x</span></span>
<span class="line"><span></span></span></code></pre></div></div><p>The trigger was fairly mundane: A batch job opened a transaction against a hot table, took row locks, and then held them for fifteen minutes without committing. A common application bug, the kind that eventually sneaks into many large codebases.</p><p>What happened around this long transaction is the interesting bit! The queries piling up behind that transaction were mostly not blocked on its locks at all. They were simple reads, where InnoDB didn&#x27;t need to wait for row locks; it reads a consistent snapshot instead. Building that snapshot means walking back through the version history of every row the open transaction had touched, and that history grew for fifteen straight minutes.</p><p>This caused reads that normally took milliseconds to start blowing through their 90-second execution ceilings. The application retried them in a tight loop. Within minutes, more than ten thousand requests were piled up inside the storage engine. Processing each required reconstructing ever-longer version chains, and the resulting page reads outpaced the buffer pool&#x27;s ability to free memory.</p><p>Now requests that had nothing to do with the locked rows, that touched entirely different tables, began failing too. The otherwise correctly sized buffer pool suddenly became too small to serve the crowd of queries.</p><p>At PlanetScale, we run our MySQL databases with Vitess, whose configured transaction timeout eventually killed the long-running transaction. Its locks were released, and the sixteen-minute backlog drained in about thirty seconds.</p><p>One slow transaction should not take down a database, and much of Vitess&#x27;s plumbing is built around avoiding exactly this situation. So what went wrong? This was caused by the combination of the single long-running query and the ten thousand requests allowed in after.</p><h2 id="migration-from-cloud-sql-to-vitess"><a href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql#migration-from-cloud-sql-to-vitess">Migration from Cloud SQL to Vitess</a></h2><p>Some context on how a seemingly healthy database could end up like this in the first place.</p><p>This workload had recently been migrated onto PlanetScale from Cloud SQL, which ran Managed Connection Pooling, a thread-pool-style layer, in front of the database. A thread pool caps how many statements execute at once, commonly around a thousand, and queues the rest. A client arriving when the pool is full waits for a slot, usually for milliseconds, occasionally for a few hundred milliseconds. This cap can protect InnoDB&#x27;s internals even under extreme load.</p><p>On PlanetScale, every MySQL shard sits behind a Vitess proxy called <a href="https://vitess.io/docs/reference/programs/vttablet/">vttablet</a>. These have a transaction pool, which caps how many transactions can be open against MySQL at once. Unlike a thread pool, when this pool fills, requests wait a set amount of time and then <em>fail with an error</em>.</p><p>For this workload, that small difference made the issue way worse. Errors propagated up an application stack that had never needed to handle them (since the Cloud SQL thread pool queued requests, pool-full errors effectively didn&#x27;t exist there).</p><p>The solution that was initially tried was to <strong>raise the transaction pool cap and increase the timeout</strong>.</p><p>The pool was increased to ten thousand, a number chosen not by sizing but because it made the errors stop. However, this now limited Vitess&#x27;s ability to control the backpressure between the application and the database storage engine.</p><h2 id="why-high-concurrency-reduces-mysql-throughput"><a href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql#why-high-concurrency-reduces-mysql-throughput">Why high concurrency reduces MySQL throughput</a></h2><p>Picture database transactions as items flowing down a conveyor belt (Factorio, anyone?). If they never interacted, throughput would scale in a straight line: twice the items in flight, twice the work done. That is the dream of a perfectly <a href="https://en.wikipedia.org/wiki/Shared-nothing_architecture">shared-nothing</a> system, but rarely is that achieved. Somewhere on the belt there is a junction where multiple conveyor belts meet. In MySQL, this is the equivalent of a hot row, a latch, a CPU run queue, etc.</p><p>Below is a simple playground to visualize how request rate and queuing impact latency. Adjust the <code>ARRIVALS / S</code> slider to see the impact.</p><iframe src="https://planetscale.com/blog/doing-more-with-less/iframe#junction" title="Interactive: rising load against a single junction" loading="lazy"></iframe><p><a href="https://en.wikipedia.org/wiki/Little%27s_law">Little&#x27;s Law</a> describes this well. In steady state, <code>N = X * W</code>. Work-in-flight equals throughput times the time each request spends inside the database. Rearranged, <code>X = N / W</code>.</p><p>Adding in-flight requests raises throughput only as long as query execution time holds steady. If each new arrival stretches the execution/wait time of all the other queries within the database, the denominator now grows along with the numerator.</p><p>Just how badly can that go? <a href="https://www.perfdynamics.com/Manifesto/USLscalability.html">Gunther&#x27;s Universal Scalability Law</a> splits the cost of concurrency into three terms:</p><div class="code-block"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>                 γN</span></span>
<span class="line"><span>X(N) = ───────────────────────────</span></span>
<span class="line"><span>        1 + α(N−1) + βN(N−1)</span></span>
<span class="line"><span></span></span></code></pre></div></div><ul><li><code>N</code> is how much work you allow to run at once.</li><li><code>γ</code> is the ideal single-request throughput: the linear scaling coefficient if requests never interacted.</li><li><code>α</code> (contention) is the cost of taking turns for a shared resource.</li><li><code>β</code> (coherency) is the cost of requests making <em>each other</em> slower. Put differently, the work a database must do to keep shared state consistent across everything in flight.</li></ul><p>Note the multiplier <code>N(N−1)</code>, which grows with the number of <em>pairs</em> of requests. When <code>N</code> doubles, this coherency cost roughly quadruples.</p><p>Past a critical point, <code>N_max = √((1−α)/β)</code>, <code>β</code> dominates, and total throughput actually starts to reverse. Gunther calls it retrograde scaling: beyond the peak, every request you admit requires coordination overhead and makes everything slower.</p><iframe src="https://planetscale.com/blog/doing-more-with-less/iframe#usl-curve" title="Interactive: the Universal Scalability Law curve" loading="lazy"></iframe><p>Let&#x27;s look at how the problem from earlier maps to this equation. The row locks were <code>α</code>. The transactions that wanted those rows had to take turns, and no amount of concurrency changed the queue&#x27;s drain rate. <code>β</code>, the real issue, was InnoDB trying to process so many queries at once: ten thousand concurrent snapshot reads, each made more expensive by the version history every <em>other</em> in-flight transaction was generating. The per-request cost rose with the increase in in-flight requests.</p><p>This is why focusing on &quot;lock contention&quot; does not explain the issue. The lock is just one of many potential junctions. It is the damage <em>around</em> the junction, the mutual slowdown of everything admitted past it, that scales with the square of concurrency and turns a slow batch job into a wider outage.</p><h2 id="reducing-pool-size-and-implementing-queuing"><a href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql#reducing-pool-size-and-implementing-queuing">Reducing pool size and implementing queuing</a></h2><p>To solve this, we implemented the reverse of the change that set this up, in both dimensions at once:</p><ol><li>Reduce the Vitess transaction pool size from ten thousand to roughly the thousand the old thread pool used. We now did this consciously and with evidence-backed proof: that workload had run successfully for years with a pool of that size.</li><li>Instead of erroring after waiting, a transaction arriving at a full pool now queues for a slot with a longer timeout.</li></ol><p>In other words, we configured Vitess&#x27;s vttablet layer to mimic the old thread pool&#x27;s behavior.</p><p>For this to be safe, two things must be true. The clients have to tolerate periodic waiting during bursts and the wait itself has to be bounded, so that a truly undersized system announces itself with timeouts at the queue rather than accumulating latency forever. Both are true for this workload.</p><p>The morning after rolling out this new configuration, it met its first test. A traffic burst arrived on a different Vitess shard of the same database, one that handles an order of magnitude more steady traffic. At peak, the pool handled around twenty-five thousand transaction pool slot requests <em>per second</em> against its thousand-odd available slots. Here are the results, in the same view we opened with:</p><div class="code-block"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span>minute   slot requests/s   errors/min   queries/s</span></span>
<span class="line"><span>  0             3,000           0         58,000</span></span>
<span class="line"><span> 10             9,000           0         61,000</span></span>
<span class="line"><span> 20            26,000           0         60,000   &#x3C;- peak pressure, throughput flat</span></span>
<span class="line"><span> 30            17,000           0         59,000</span></span>
<span class="line"><span> 40             8,000           0         62,000</span></span>
<span class="line"><span> 50             4,000           0         60,000</span></span>
<span class="line"><span> 60             2,000           0         57,000   &#x3C;- burst drained, queue empty</span></span>
<span class="line"><span></span></span></code></pre></div></div><p>During the earlier incident, errors climbed to 1,400 a minute and throughput fell to a tenth of what the workload requested.</p><p>With the new and improved configuration, the application experienced no disruption and QPS didn&#x27;t drop.</p><p>Over the entire day, database health was much better:</p><ul><li><strong>One rejected transaction.</strong> Despite slot requests/s at times jumping as high as 40,000, the queue absorbed effectively everything. Waits sat comfortably within the timeout.</li><li><strong>Fewer than two hundred statements executing inside MySQL at any instant.</strong> InnoDB managed the bursts well. Sixty thousand queries a second through fewer than two hundred concurrent slots is Little&#x27;s Law again: about three milliseconds apiece.</li><li><strong>Lock contention didn&#x27;t change.</strong> The hot-row pattern was just a minor contributor here. We were able to solve the problem without changing the lock contention dynamics.</li></ul><p>This allowed the database to do less work <em>at once</em>, allowing it to do more work in total.</p><p>Below is another playground to visualize the difference between an open request flow and a gated one. Send bursts of traffic, simulate a locking event, and see the difference between a gated and ungated solution with the buttons below.</p><iframe src="https://planetscale.com/blog/doing-more-with-less/iframe#before-after" title="Interactive: same workload, opposite overflow behavior" loading="lazy"></iframe><h2 id="when-to-limit-database-concurrency"><a href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql#when-to-limit-database-concurrency">When to limit database concurrency</a></h2><p>This is not a universal recommendation to tightly limit concurrency. Its applicability must be understood in the context of your workload.</p><ul><li>Workloads built on pessimistic locking and contended shared state.</li><li>Hot rows updated by many workers.</li><li><code>SELECT ... FOR UPDATE</code> on popular keys.</li><li>Long transactions holding locks while they do unrelated work.</li><li>Counters, balances, job queues.</li></ul><p>If that describes your workload, added concurrency might just decrease your throughput!</p><p>The principles described here are not MySQL-specific. Postgres can have similar problems, but also a similar solution: <a href="https://planetscale.com/docs/postgres/connecting/pgbouncer">PgBouncer</a>&#x27;s transaction pooling exists to allow for more client concurrency than server concurrency. This is why our <a href="https://planetscale.com/docs/postgres">Postgres offering</a> comes with a local PgBouncer, and can be configured with dedicated primary and replica PgBouncer instances too.</p><h2 id="building-systems-that-handle-backpressure"><a href="https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql#building-systems-that-handle-backpressure">Building systems that handle backpressure</a></h2><p><strong>Backpressure can&#x27;t always be avoided, so make conscious choices about the expected behavior.</strong> The answer is almost never to simply raise a limit out of reach, but to fix what happens when that limit is reached.</p><p>Sometimes the fastest thing you can do for a busy system is to simply let less happen at once.</p>]]></content>
    <summary><![CDATA[Increasing throughput sometimes requires reducing parallelism. A look into why this is the case for Vitess + MySQL databases]]></summary>
  </entry>
  <entry>
    <title>Massively parallel Postgres backups</title>
    <link href="https://planetscale.com/blog/massively-parallel-postgres-backups"/>
    <id>https://planetscale.com/blog/massively-parallel-postgres-backups</id>
    <published>2026-07-31T00:00:00.000Z</published>
    <updated>2026-07-31T00:00:00.000Z</updated>
    <author>
      <name>Ben Dicken</name>
    </author>
    <category term="engineering"/>
    <category term="neki"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#hero-sharded-database" title="/blog/massively-parallel-postgres-backups/iframe#hero-sharded-database" loading="lazy"></iframe><p>Every 12 hours, a backup system must turn the entire state of a busy database into a consistent, encrypted snapshot, with no impact to production queries.</p><p>Such backups are crucially important and simultaneously something that most engineers would rather never have to think about.</p><p><em>Just make the backup work</em>.</p><p>Our goal at PlanetScale is to make taking, scheduling, managing, and restoring Postgres and MySQL backups effortless.</p><p>Though this is what our customers experience from the outside, achieving this internally requires careful orchestration of cloud infrastructure and DBMS tooling.</p><p>It&#x27;s especially interesting to look at backups for sharded databases, which requires spinning up backup-specific nodes, pulling data from object storage, and WAL replay, all with massive parallelism. These techniques allow for petabyte-scale databases to be backed up in hours, at rates over 50 GB/s.</p><p>Here we take a behind-the-curtain look at how to effectively back up a sharded database with massive parallelism.</p><h2 id="the-backup-lifecycle"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#the-backup-lifecycle">The backup lifecycle</a></h2><p>Here is an example of a Neki (sharded Postgres) database with 8 shards, happily handling hundreds of thousands of queries per second of production traffic.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database" loading="lazy"></iframe><p>If sharding is a new concept to you, check out our recent post <a href="https://planetscale.com/blog/making-768-servers-look-like-1">Making 768 servers look like 1</a> on how it all works. The first step in taking a backup depends on whether this is the <em>very first backup</em> or if we&#x27;ve taken one previously. We&#x27;ll start with the steady-state case, which assumes a prior healthy backup already captured and stored in Amazon S3 (or similar object storage in other clouds).</p><p>Since a sharded Postgres database is many individual primary Postgres servers working together, we use regular Postgres backups as the building block for large-scale backups with Neki.</p><p>There are three ways to take backups in Postgres, which we&#x27;ve <a href="https://planetscale.com/blog/postgres-backups-under-the-hood">written about in detail previously</a>. The best of the three, and the one that Neki uses, is combining filesystem backups with the replay of archived Write-Ahead Log (WAL). To summarize, the steps for this are:</p><ol><li>Begin a full backup of the on-disk Postgres database files at time <code>T1</code></li><li>The backup completes at time <code>T2</code>; between <code>T1</code> and <code>T2</code>, rows on disk may have been mutated</li><li>Replay the write-ahead log modifications between <code>T1</code> and <code>T2</code> to correct mutated data</li><li>Store the final result in a distinct storage location like Amazon S3</li></ol><p>We <em>could</em> complete these steps directly on the primary, or perhaps one of the traffic-serving replicas. The problem is that this task utilizes a significant amount of IOPS and compute, especially for a large database. Our goal should be to minimize the impact a backup will have on production query serving.</p><p>Because of this, we take a different approach. We spin up a <span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">brand new set of EC2 instances</span>, one per shard, to manage the backups on.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-backup-servers" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-backup-servers" loading="lazy"></iframe><p>These <span class="bg-yellow-100/70 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-300">new instances</span> will be responsible for the majority of the work in the backup. Because we operate these sharded databases in clouds like AWS and GCP, dynamically spinning up tens or hundreds of instances for short time slices to complete backups is achievable. It adds a small amount of cost, but is worth it to minimize negative production impact.</p><h2 id="reusing-old-backups"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#reusing-old-backups">Reusing old backups</a></h2><p>The next step is restoring the most recent backup to each shard. Prior backups are stored in object storage. Here we will use <span class="bg-orange-100/80 text-orange-700 dark:bg-orange-800/70 dark:text-orange-200">Amazon S3</span> as the example, but the same applies to other clouds (like GCS in Google Cloud). These are streamed directly from here.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-s3-copy" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-s3-copy" loading="lazy"></iframe><p>This approach requires temporary compute and transfers each shard&#x27;s data out of and back into object storage. We accept that cost for two important reasons:</p><ul><li>Only recent WAL comes from the primary, minimizing production impact</li><li>Every cycle proves the previous backup can be restored and replayed</li></ul><p>Once all the copying completes, these 8 servers have the exact state of each of the 8 shards from the previous backup, 12 hours ago.</p><h2 id="replaying-the-wal"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#replaying-the-wal">Replaying the WAL</a></h2><p>We now must catch up each shard&#x27;s old backup to match the present state of the database. This requires replaying all changes from the Postgres Write-Ahead Log between 12 hours ago and now.</p><p>A naïve approach would be to pull the WAL directly from the primary. This is problematic for several reasons:</p><ol><li>This would have nontrivial production impact. Replaying 12 hours of WAL on a high-churn database could take tens of minutes, or even an hour+.</li><li>Because we don&#x27;t want too much server storage consumed by WAL, we continuously archive it to S3. Thus, we likely don&#x27;t even have the full past 12 hours of WAL resident on the primary node.</li></ol><p>On PlanetScale, all Postgres databases use <a href="https://wal-g.readthedocs.io/PostgreSQL/"><code>wal-g</code></a> to <span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">continuously archive</span> their write-ahead logs. A sharded Neki database is similar, except each shard has a distinct WAL archive stream.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-continuous-wal-archive" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-continuous-wal-archive" loading="lazy"></iframe><p>What if we streamed from there instead?</p><p>This almost solves the problem. The remaining problem is that Postgres archives WAL only after a segment is complete. If write traffic doesn&#x27;t fill a segment sooner, our five-minute <code>archive_timeout</code> setting forces a segment switch so it can be archived. This means the newest changes may not have reached S3 yet. Thus, we take a hybrid approach. The system uses S3 for the majority of the replay, then streams the last ~few minutes of changes directly from the primary. Ideally, this last step takes on the order of seconds, not minutes or hours.</p><p>Putting this step together, back in our sharded database:</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-wal-catch-up" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-wal-catch-up" loading="lazy"></iframe><h2 id="completing-the-cycle"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#completing-the-cycle">Completing the cycle</a></h2><p>When every node has caught up replication to time <code>T</code>, where <code>T</code> is the timestamp we will log for the backup time, we stop WAL replication, freezing the point-in-time of the backup. Time <code>T</code> is saved to ensure we know the precise time, down to the second, included in this backup. The full backup is now consistent, with no smeared data. The final step is to encrypt and send these backups off to a new S3 bucket for safe keeping.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-s3-offload" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-s3-offload" loading="lazy"></iframe><p>When complete, the backup nodes have served their purpose, and are decommissioned.</p><h2 id="the-initial-backup"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#the-initial-backup">The initial backup</a></h2><p>The cycle just described assumed we had a good backup from 12 hours prior to start with. In the steady-state this is true, but not for a brand new database.</p><p>Within 12 hours of a new database creation, <code>pg_basebackup</code> is used to seed a backup node for each shard.</p><p><code>pg_basebackup</code> is a built-in PostgreSQL client utility that takes a physical backup of the entire database cluster: the data directory, tablespaces, and configuration needed for recovery.</p><p>Why not upload the <code>pg_basebackup</code> output directly? We use it only to seed the temporary backup nodes. The durable backup is created with <code>wal-g</code>, keeping the format and restore process identical for initial and steady-state backups.</p><p>The steps for this first one are similar, but slightly different than the steady-state:</p><ol><li>Spin up one new EC2 instance per shard.</li><li>Run <code>pg_basebackup</code> on each instance to copy data from its primary.</li><li>Configure each instance to replicate from its primary.</li><li>Replay WAL until every instance catches up.</li><li>Stop replication at time <code>T</code>.</li><li>Use <code>wal-g</code> to format, encrypt, and upload each backup to S3.</li></ol><p>Once this exists, all future backups happen with the restore -&gt; catch up -&gt; save flow.</p><h2 id="need-for-speed"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#need-for-speed">Need for speed</a></h2><p>Part of the reason we do so much parallelism is due to the nature of sharding. Whether it&#x27;s 4 shards or 400, since each shard contains its own Postgres primary, we may as well take a rock-solid system (regular Postgres backups) and do it over and over on each shard.</p><p>A side-effect of this is that backups are VERY fast, even for large databases.</p><p>For every backup, the data transfer steps are:</p><ol><li>Restore old backup from S3</li><li>Catch up the backup using a hybrid of S3 + the primary</li><li>Send the new file back to S3</li></ol><p>Consider what it would take to complete this backup cycle on an <em>unsharded</em> 32 terabyte database. Backups on PlanetScale are compressed and stored with encryption at rest in S3. We&#x27;ll assume that backups for this 32 terabyte database compress down to 20 terabytes. Therefore, we have</p><ol><li>20,000 GB transfer from S3</li><li>Catch up (Ex: 20 GB, compressed to 10 GB)</li><li>20,010 GB back to S3</li></ol><p>This equates to a total transfer volume of ~40,030 GB. If our various nodes and interconnects can sustain a 500 MBps transfer rate, this means the backup would take ~22 hours. Crucially, this means that if we want twice-daily backups, multiple backups would overlap. This would prevent us from meeting our recovery point objective (RPO).</p><p>This same database in Neki, spread over 8 shards storing ~4 TB each, performs quite differently. In total, we need to transfer, catch up, and store the same 40,030 GB. But if we can do so across 8 distinct backup nodes in parallel, each capable of 500 MBps, we reduce the total time to ~2.8 hours.</p><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#parallel-backup-speed" title="/blog/massively-parallel-postgres-backups/iframe#parallel-backup-speed" loading="lazy"></iframe><p>This gets better the more shards you add. The same data spread across 32 shards would back up in 42 minutes. Backup speed scales well. 100 terabytes on 100 shards backs up at ~the same speed as 1 terabyte on a single shard.</p><h2 id="what-are-backups-used-for"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#what-are-backups-used-for">What are backups used for?</a></h2><p>One reason to take backups is data safety. In the case of accidental data deletion or a one-in-a-million database disaster, backups (and the WAL) are an essential fallback.</p><p>But at PlanetScale, backups are also a core part of everyday database operations. For <a href="https://planetscale.com/metal">Metal</a> databases specifically (ones with Local NVMe), backups are also used every time the database is resized.</p><p>Resizing a Metal database requires:</p><ol><li>For every existing node in the sharded database, spin up a brand-new EC2 instance at the new size. In a database with 8 shards each with a primary and 2 replicas, we may currently be running it on <code>8 x 3 = 24</code> <code>i8g.xlarge</code> nodes. While running, we initialize 24 additional <code>i8g.2xlarges</code> to double the compute capacity.</li><li>Each of the 24 new nodes pulls down a copy of the most recent backup from S3, and begins catching up to the present state of the database via the archived WAL.</li><li>All nodes are initialized as standbys of the original primary, and complete final replication catch-up.</li><li>When all nodes are synchronized with the primary, a switchover is made from the old <code>i8g.xlarge</code> primary to a selected new <code>i8g.2xlarge</code> primary.</li><li>The smaller nodes are decommissioned, leaving only the larger primary and replicas.</li></ol><iframe src="https://planetscale.com/blog/massively-parallel-postgres-backups/iframe#sharded-database-resize-cycle" title="/blog/massively-parallel-postgres-backups/iframe#sharded-database-resize-cycle" loading="lazy"></iframe><p>We&#x27;ve now completed a full resize across all shards. Backups are used to facilitate the creation / catch-up of the new nodes. This process can take anywhere from minutes to hours, depending on how large the backups are and how much WAL there is to replay.</p><p>Backups are also used when a node needs to be replaced due to unexpected failure. If your average server lifetime is 5 years, you&#x27;ll rarely notice a failure on a small database with a single primary. For a database with hundreds of shards, each with multiple replicas, the statistical likelihood for node failure in a given week or month increases significantly. When we see an individual node fail (say, a single replica in a shard), the process for replacement looks like:</p><ol><li>Initialize a new cloud instance of the same size as the previously failed one.</li><li>Use the process described earlier to restore a recent back-up to it, catch up the WAL.</li><li>Initialize as a follower of the original primary, synchronize data.</li><li>This new node can now actively serve read queries and/or serve as a new primary during a switchover or failover operation.</li></ol><h2 id="what-about-mysql"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#what-about-mysql">What about MySQL?</a></h2><p>Everything here has been about how we complete sharded backups and resizes on Postgres databases, powered by Neki. PlanetScale also operates large-scale sharded MySQL databases, powered by Vitess as our query routing / sharding layer.</p><p>The process for backing up and resizing these databases is quite similar! We have a <a href="https://planetscale.com/blog/faster-backups-with-sharding">whole separate blog</a> on how this works, but the main difference is that we use <code>VTBackup</code> instead of the Postgres backup builtins, we use the <a href="https://dev.mysql.com/doc/refman/en/binary-log.html">MySQL binary log</a> replication instead of WAL, and replication catchup is done from the primary instead of a hybrid between S3 and primary.</p><h2 id="make-it-boring"><a href="https://planetscale.com/blog/massively-parallel-postgres-backups#make-it-boring">Make it boring</a></h2><p>Ultimately, we want all this to be transparent to you. Taking a backup should be as easy as an automated schedule, or clicking a button. Resizing a database should be a single click or API call.</p><p>However, we also have a deep appreciation for the finer details of database operations. By taking a journey through the lifecycle of backups, we hope you have a new-found appreciation for how incredible database orchestration is.</p><p>If you find this fascinating, we&#x27;re always looking for <a href="https://planetscale.com/careers">talented database engineers</a>.</p>]]></content>
    <summary><![CDATA[PlanetScale backs up petabyte-scale sharded Postgres databases in hours using parallel infrastructure, object storage, and WAL replay.]]></summary>
  </entry>
  <entry>
    <title>Postgres backups under the hood</title>
    <link href="https://planetscale.com/blog/postgres-backups-under-the-hood"/>
    <id>https://planetscale.com/blog/postgres-backups-under-the-hood</id>
    <published>2026-07-24T00:00:00.000Z</published>
    <updated>2026-07-24T00:00:00.000Z</updated>
    <author>
      <name>Josh Brown</name>
    </author>
    <category term="engineering"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<p>Data integrity is one of the most important pillars for a database system. If your database goes down, backups are how you stay safe and recover in case of disaster.</p><p>Backups should be taken often, and recovery needs to stay fast. With backups being such a vital part of keeping your data safe, you should know how they work.</p><h2 id="the-types-of-postgres-backups"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#the-types-of-postgres-backups">The types of Postgres backups</a></h2><p>There are <a href="https://www.postgresql.org/docs/current/backup.html">three different ways</a> to back up a Postgres database: Logical backups (<code>pg_dump</code>), file system backups, and continuous archiving.</p><h2 id="logical-backups"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#logical-backups">Logical backups</a></h2><p><code>pg_dump</code> is a purely logical backup that exports a consistent snapshot of the database, and is the simplest way to back up a Postgres database. At its core, <code>pg_dump</code> reads the current state of the database and writes the data to a given output.</p><p><code>pg_dump</code> can create dumps in either plain text as SQL statements (with SQL queries such as COPY or INSERT), or custom <code>pg_dump</code> formats designed for use with <code>pg_restore</code>.</p><p>Regardless of the format used, the data <code>pg_dump</code> outputs is instructions on how to rebuild the data in the database.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- An abbreviated snippet of output from pg_dump in plain format</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">CREATE</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TABLE</span><span style="--shiki-light:#5E49AF;--shiki-dark:#B7A5FB"> public</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">.customers (</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">bigint</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">    name</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> text</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    email </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">text</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    country </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">text</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">,</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    created_at </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">timestamp with time zone</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> NOT NULL</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">ALTER</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TABLE</span><span style="--shiki-light:#7D5903;--shiki-dark:#FED54A"> public</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">.</span><span style="--shiki-light:#7D5903;--shiki-dark:#FED54A">customers</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> OWNER</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TO</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> postgres;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">COPY</span><span style="--shiki-light:#7D5903;--shiki-dark:#FED54A"> public</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">.</span><span style="--shiki-light:#7D5903;--shiki-dark:#FED54A">customers</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> (id, </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">name</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, email, country, created_at) </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">FROM</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> stdin;</span></span>
<span class="line"><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">1</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">	Customer </span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">1</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">	user1@example.com	UK	</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">2020</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">01</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">02</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 00</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">+</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span></span>
<span class="line"><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">2</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">	Customer </span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">2</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">	user2@example.com	DE	</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">2020</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">01</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">03</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 00</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">+</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">00</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">...</span></span>
<span class="line"></span></code></pre></div></div><p>Reading, formatting, and compressing a few gigabytes is trivial, but in the terabytes+ this becomes increasingly unfeasible. Even if your system can take a successful <code>pg_dump</code> backup at 30TB, it consumes a lot of resources. User queries must contend with <code>pg_dump</code> for CPU and IO, and in worst case scenarios this brings the database to a crawl.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>Logical backups lack the ability to do point in time recovery (PITR) since the backup is a static snapshot of data at a given time. However, they are particularly useful for facilitating migrations.</p></div><p><code>pg_dump</code> connects to the database through a standard SQL connection, only saving the rows and schema inside the database. It does not save physical page layouts, dead tuples, stored index bytes, or cluster-wide roles/tablespaces. This generally keeps outputs from <code>pg_dump</code> smaller compared to other backup methods.</p><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/pg-dump/iframe" title="pg_dump serializing rows into a dump file and restoring them into a fresh database" loading="lazy"></iframe><h3 id="logical-backups-and-transaction-wraparound"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#logical-backups-and-transaction-wraparound">Logical backups and transaction wraparound</a></h3><p>Logical backups leverage Postgres&#x27;s MVCC support in order to take a consistent backup while the database is online. If you&#x27;re not familiar with MVCC, read our blog on <a href="https://planetscale.com/blog/postgresql-mvcc">Postgres MVCC</a>. The section on <a href="https://planetscale.com/blog/postgresql-mvcc#the-transaction-horizon">transaction horizons</a> is particularly important here. When <code>pg_dump</code> starts, it opens a transaction in one of two ways:</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- pg_dump with the default settings</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">BEGIN</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SET</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TRANSACTION</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> ISOLATION</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> LEVEL</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> REPEATABLE</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> READ</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">READ</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> ONLY;</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">...</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- pg_dump --serializable-deferrable</span></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- Ensures transactions are serializable while the backup is being taken.</span></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- Generally not recommended for backups intended for disaster recovery.</span></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- see the --serializable-deferrable flag for pg_dump</span></span>
<span class="line"><span style="--shiki-light:#818181;--shiki-dark:#A1A1A1">-- https://www.postgresql.org/docs/current/app-pgdump.html</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">BEGIN</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SET</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> TRANSACTION</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> ISOLATION</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> LEVEL</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> SERIALIZABLE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">READ</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> ONLY, DEFERRABLE;</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">...</span></span>
<span class="line"></span></code></pre></div></div><p>Every transaction that modifies data gets assigned an internal id, referred to as its <code>xid</code>. Rows in the database contain a hidden variable named <code>xmin</code>. <code>xmin</code> informs Postgres of the <code>xid</code> for the transaction that modified that row, while <code>xmax</code> is which transaction deleted, updated or locked that row.</p><p>Although <code>pg_dump</code> may not get its own <code>xid</code>, it pins a transaction snapshot of the active <code>xid</code>s at that moment, giving the dump a consistent view of the database for its entire duration.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p><code>txid</code> is the 8 byte ID used for tracking transactions that modify data. <code>xid</code> is the lower 4 bytes of <code>txid</code> and is stored in tuples on disk as <code>xmin</code>, <code>xmax</code>, or in other variables.</p></div><p>Remember that <code>xid</code>s are 32-bit integers used to track the historical version of rows (or tuples, technically) across transactions. Tuples are the underlying representation of a row on disk. 32 bits can only represent a maximum of ~4 billion numbers, so what happens after we have more than 4 billion transactions?</p><p>Postgres treats <code>xid</code>s as a circular range. Quoting from <a href="https://www.depesz.com/2024/03/18/what-the-hell-is-transaction-wraparound/">depesz&#x27;s article</a> on transaction wraparound:</p><blockquote><p>Let&#x27;s explain this bit on smaller numbers. Let&#x27;s assume that we have the whole range of 0 to 10. Only 11 xids. If my current xid is 5, then xids 0..4 are in the past, and 6..10 are in the future. And if current xid is 3? Future is simple: 4..8. But past now contains two ranges: 0..2, and 9..10. Similarly, if current xid is 9, then past xids will be 4..8, and future would be 10, 0..3.</p><p><a href="https://www.depesz.com/2024/03/18/what-the-hell-is-transaction-wraparound/">depesz</a></p></blockquote><p>To keep the range circular and <code>xid</code>s reusable, Postgres uses a freeze bit. This bit marks a tuple as older than any running transaction or snapshot, and should always be read as an &quot;old&quot; tuple.</p><p>When <code>autovacuum</code> runs, it internally calls an internal Postgres function <code>GetOldestNonRemovableTransactionId()</code>. This finds the oldest <code>xid</code> that is assigned to an open transaction, otherwise known as the transaction horizon, and <code>autovacuum</code> will go and freeze tuples &quot;older&quot; than that given xid.</p><p>A unique situation arises when the following criteria are met:</p><ul><li>A long running open transaction pins the transaction horizon</li><li>More than ~2 billion new transactions have started after the long running transaction</li></ul><p>If both of these points are met, new transactions start seeing old unfrozen tuples as if they were written by future transactions. This is called a transaction wraparound. At this point, Postgres will refuse new commands that would create <code>xid</code>s, triggering read-only mode to ensure no data loss actually occurs.</p><p>Since <code>pg_dump</code> holds its snapshot open for the entire duration of the backup, it prevents the transaction horizon from advancing until the backup finishes. On a large database with sufficiently high throughput, <code>pg_dump</code> may inadvertently trigger this phenomenon bringing the cluster into read-only mode.</p><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/xid-wraparound/iframe" title="Interactive: a long-running pg_dump pins the freeze horizon until Postgres refuses to assign new XIDs" loading="lazy"></iframe><p>Above, we can see this in action. Each transaction has its own &quot;future&quot; and &quot;past&quot; based off its own <code>xid</code>. Every time <code>autovacuum</code> runs, it advances the oldest unfrozen <code>xid</code>. The blue section represents active <code>xid</code>s that have not yet been frozen.</p><p>When the cluster goes into read-only mode in our visual, new transactions would potentially cause data loss. A new transaction would see unfrozen tuples created by the first transaction after <code>pg_dump</code> ran as if they were created by a future transaction that has not yet run.</p><p>On very large clusters (upwards of 30TB), this is a valid concern depending on the write throughput to the database. This isn&#x27;t to say <code>pg_dump</code> is bad by any means. <code>pg_dump</code> is an extremely useful tool. The point here is demonstrating that for taking scheduled backups, <code>pg_dump</code> is most likely not the correct tool.</p><h2 id="file-system-backup"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#file-system-backup">File system backup</a></h2><p>Another approach is to perform a full file system backup. This is substantially faster than <code>pg_dump</code> for saving and restoring a backup.</p><p>A file system backup bypasses the database and directly copies the underlying file system.</p><p>In order for file system backups to be usable however, the database must be completely shut down. If the database is online during a backup, rows could be changed after the backup process has started, but before they have been copied, leaving the database in an inconsistent state.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>It is possible to take a file system backup online, only if the underlying file system supports atomic file system snapshots. See the Postgres docs on <a href="https://www.postgresql.org/docs/current/backup-file.html">file system backups</a> for more details.</p></div><p>This is explained in more detail in the next section, as this is what continuous archiving solves.</p><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/filesystem-backup/iframe" title="A file system backup requiring the database to be shut down while files are copied" loading="lazy"></iframe><p>The other drawback to this type of backup (and logical backups) is that changes to data after a backup has been taken are not included in the backup. If data from between &quot;now&quot; and the most recent backup needs to be recovered, it isn&#x27;t there.</p><p>The solution to this is to save the WAL, along with the file system, which is exactly what continuous archiving does.</p><p>Offline file system backups are generally a non-starter for production workloads, as no one wants to take down their database for hours to perform a daily backup.</p><h2 id="continuous-archiving"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#continuous-archiving">Continuous archiving</a></h2><p>The most powerful (and complicated) backup method is continuous archiving. Continuous archiving combines a file system backup with the write-ahead log (WAL).</p><p>This process can be done without downtime and generally lower impact on the CPU of the current system. This is what PlanetScale uses to facilitate Postgres and Neki backups.</p><p>The first step of taking a continuous archive backup has already started long before you take your first backup: archiving the WAL.</p><h3 id="the-wal"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#the-wal">The WAL</a></h3><p>The WAL is like your database&#x27;s logbook. Every change, addition, or deletion of the data in your database is written down in the WAL.</p><p>Before Postgres makes any changes to data on disk, it first records the changes in the WAL. This ensures that upon a system crash, Postgres can restore to a consistent state by replaying the data mutations saved in its WAL. See the <a href="https://en.wikipedia.org/wiki/Algorithms_for_Recovery_and_Isolation_Exploiting_Semantics">ARIES Wikipedia</a> for more information on the algorithm and reasons behind WAL.</p><p>The WAL is by default saved in 16MB segments as binary files, however the size is configurable per cluster. These can be fed to tools such as <code>pg_waldump</code> yielding human readable output:</p><div class="code-block" data-language="go"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap2</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">       len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">     60</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">  7136</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">          0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C12FF8</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C12F80</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> PRUNE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> snapshotConflictHorizon</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 771</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> nredirected</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> ndead</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 15</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16387</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">FPW</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">);</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> hole</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> offset</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 452</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> length</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1116</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">        len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    119</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">  6443</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C14BF0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C12FF8</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> HOT_UPDATE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 44</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_infobits</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> [],</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> flags</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> 0x</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">10</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 85</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16387</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">FPW</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">);</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> hole</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> offset</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 364</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> length</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1868</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">        len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    114</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">   114</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16538</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C14BF0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> HOT_UPDATE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 45</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_infobits</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> [],</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> flags</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> 0x</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">10</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 86</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16387</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">...</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">...</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">...</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">        len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    106</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">   106</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16F78</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16F08</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> HOT_UPDATE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 82</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_infobits</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> [],</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> flags</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> 0x</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">10</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 108</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16387</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Transaction</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">     46</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    46</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        778</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16FE8</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16F78</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> COMMIT</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 2026</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">07</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">-</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">21</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 02</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">22</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">15.866163</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> UTC</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">        len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    107</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">   807</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        779</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C17018</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C16FE8</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> HOT_UPDATE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 779</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_infobits</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> [],</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> flags</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> 0x</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">10</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 2</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16399</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">FPW</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">);</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> hole</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> offset</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 508</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> length</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 7492</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rmgr</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> Heap</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">        len</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">rec</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">tot</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">):</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">    107</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">   911</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> tx</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">        779</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> lsn</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C17340</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> prev</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#C11027;--shiki-dark:#FF909F">01C17018</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> desc</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> HOT_UPDATE</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 779</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 2</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> old_infobits</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> [],</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> flags</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> 0x</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">10</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_xmax</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> new_off</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 3</span></span>
<span class="line"><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">	blkref</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> #</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">0</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> rel</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 1663</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16384</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">/</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">16399</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> fork</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> main</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> blk</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 8</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> (</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600">FPW</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">);</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> hole</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> offset</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 500</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span><span style="--shiki-light:#A78103;--shiki-dark:#F2B600"> length</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 7388</span></span>
<span class="line"></span></code></pre></div></div><p>There is a lot of information here, and remember this is only what <code>pg_waldump</code> is showing us. For understanding how continuous archiving works, there are a few parts of WAL we need to understand.</p><ul><li><code>len (rec/tot)</code>: <code>rec</code> is the size in bytes of the specific record data, while <code>tot</code> is the total bytes saved in WAL for this entry.</li><li><code>lsn</code>: The log sequence number (LSN) is the byte address of the given WAL line.</li><li><code>FPW</code>: Full page write (FPW) marks where a full page was written to the WAL. This is what causes rec != tot in the <code>len</code> field.</li><li><code>rel</code>: Tablespace / db / filenode, or more simply put, the location of a table file in a specific database. Tables are represented as filenodes, which are made up of pages, and saved as files on disk. See our <a href="https://planetscale.com/blog/postgresql-mvcc">MVCC blog</a> for more detail.</li><li><code>blk</code>: The specific page of the filenode that is being updated or saved.</li><li><code>desc</code>: What action is happening. <code>HOT_UPDATE</code>, for example, is when an updated tuple with an associated index (in this case from an <code>UPDATE</code> SQL query) is saved to the same page it was originally on.</li></ul><p>By default, Postgres enables a setting called <code>full_page_writes</code>. This setting is necessary for continuous archiving, and is forced on during a file system backup (also known as a base backup). This instructs Postgres to save an entire page to WAL the first time the page is updated after a checkpoint.</p><p>If the base backup is taken from a replica or standby node, then the operator must ensure <code>full_page_writes</code> is turned on for the primary. When the backup is taken directly on the primary, <code>pg_basebackup</code> (the common CLI tool for taking continuous archives) will force it on.</p><div class="mb-3 border p-3 border-blue-600 dark:border-blue-500"><p><span class="bg-blue-600 px-sm text-white dark:bg-blue-500 dark:text-black">Note</span></p><p>Pages are chunks of data within table or index files on disk, usually 8KB per page. Pages contain the underlying tuples and pointers that materialize into rows.</p></div><p>Every WAL line above containing <code>FPW</code> marks a point where Postgres saved the entire page (<code>blk</code>) within a given filenode (<code>rel</code>) to the WAL.</p><p>If you look closely at the <code>rel</code>, <code>blk</code>, and <code>FPW</code> on each line, you can see each first update to a given page (<code>rel</code>/<code>blk</code>) after a checkpoint triggers a new full page write. This is the mechanism that makes continuous archiving possible, as we will see later on.</p><h3 id="archiving-data"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#archiving-data">Archiving data</a></h3><p>Saving WAL segments often means streaming them to external storage systems such as S3. This can be done via tools such as <code>wal-g</code>, which are built for this exact purpose.</p><p>For any Postgres cluster (including <a href="https://planetscale.com/neki">Neki</a> shards) running on PlanetScale, every primary has a <code>wal-g</code> sidecar streaming its Postgres WAL to S3.</p><p>When we start a backup, we begin by taking an online file system backup. Interestingly, we don&#x27;t care if writes or mutations cause the file system to be in an inconsistent state, or even corrupted.</p><p>The data files we copy over can be thought of as &quot;smeared&quot; data on disk since they have potentially been &quot;smeared&quot; by queries that happened during our backup. We will take a look at exactly why we don&#x27;t need to worry about smeared data with an example.</p><p>The start time is based on a LSN in the log, known as the backup&#x27;s <code>start_lsn</code>, while the <code>end_lsn</code> is the point in the WAL at which the backup completed.</p><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/continuous-archive/iframe" title="Continuous archiving: data files copied to S3 while WAL is continuously archived" loading="lazy"></iframe><p>Say we start taking a backup at some time stamp. As the backup is copying over files, the database receives a SQL statement:</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">UPDATE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> users </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SET</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> email_confirmed</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">=</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">true </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">WHERE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">=</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 314159</span></span>
<span class="line"></span></code></pre></div></div><p>where <code>email_confirmed</code> was previously false. At this point, any one of three things could happen after the query executes.</p><ol><li>The backup process has already copied the file containing the page for that row where <code>email_confirmed</code> was set to false. This is the best case; our backup is unaffected, and still in a consistent state. We have already copied the file, so we do not care if it is modified after the fact.</li><li>The backup has not yet read the page where that row lives, and our backup is now out of sync. The backup started at a time where <code>email_confirmed</code> was false, but it will save that data page while it is true!</li><li>The page is backed up at the same instant it was written to, and is now corrupted. The page now contains mangled bits, and later on we will be unable to read its contents properly.</li></ol><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/update-scenarios/iframe" title="Three possible outcomes of an UPDATE landing during an online file system copy" loading="lazy"></iframe><p>How does Postgres know how to un-smear these data files?</p><p>This is where the <code>full_page_writes</code> capability of the WAL comes into play. After we have copied over the smeared file system backup from S3 to a new container for restoring, Postgres looks over the WAL reading every change between <code>start_lsn</code> and <code>end_lsn</code>.</p><p>Since <code>full_page_writes</code> was on when Postgres received the <code>UPDATE users ...</code> query mentioned above, it created a new heap tuple on disk as usual, and additionally logged a full image of the entire 8KB page where that row lives to the WAL.</p><p>Regardless of the state of that page from the file system backup, whether it is corrupted, in an inconsistent state, or perfectly healthy, we have enough information within the WAL to reconstruct it. The full page image in the WAL can replace the same page that was originally backed up if the ordinary WAL records are not enough to repair it.</p><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/wal-page-fix/iframe" title="WAL replay healing smeared pages with full-page images" loading="lazy"></iframe><p>Notice that the actual restored backup is not a snapshot of the database when the backup was started (like how <code>pg_dump</code> works), but a consistent version of the database when it finished backing up.</p><p>If we tried to restore the database to a point in-between <code>start_lsn</code> and <code>end_lsn</code> we would potentially be left with smeared data. The restore process must read through the full WAL from start to end before the restore can be marked as healthy.</p><h3 id="point-in-time-recovery"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#point-in-time-recovery">Point in time recovery</a></h3><p>The backups system described above is also what facilitates Postgres&#x27; ability to do point-in-time restores.</p><p>This is where the &quot;Continuous&quot; part of &quot;Continuous Archiving&quot; comes into play. Since the WAL is continuously saved to S3, we can restore any backup and continuously replay the WAL past our <code>end_lsn</code> up to any arbitrary point in time.</p><p>Replaying the WAL is generally more expensive than copying over the file system, however. This is why backups need to happen often, and is why at PlanetScale we back up every cluster every 12 hours by default. This keeps the WAL replay within a reasonable bound (half of a day) and keeps restores fast.</p><p>Continuously saving WAL is what also powers cluster resizes. When you resize your cluster on PlanetScale, the following is happening under the hood:</p><ol><li>We bring up a new node with the desired specs</li><li>Copy over the file system backup (or mount an EBS volume if we are on an EBS backed node)</li><li>Replay WAL as close to the current time as possible</li><li>Stream remaining changes from the primary</li><li>Cutover to new node</li></ol><iframe src="https://planetscale.com/blog/postgres-backups-under-the-hood/cluster-resize/iframe" title="A cluster resize: the new node restores the latest backup, replays WAL, then takes over traffic" loading="lazy"></iframe><p>In fact, this is the same process we use to take backups in the first place! We use the latest backup taken (usually around 12 hours ago), bring it online, catch it up to the primary, and finally save its file system as a new backup. This is all done on a throwaway node only used to take a new backup, meaning the entire process never contends with the primary for CPU, IO, or network bandwidth.</p><h2 id="backups-at-scale"><a href="https://planetscale.com/blog/postgres-backups-under-the-hood#backups-at-scale">Backups at scale</a></h2><p>Even with these techniques in place, extremely large databases can end up with backups that take 12+ hours to complete, resulting in ever growing windows between restore points.</p><p>When restore is needed, it could take days to get the new node back online. You can attempt a PITR from your most recent backup, but that could mean waiting substantially longer for WAL to not only stream over, but replay onto a new node.</p><p>We <a href="https://planetscale.com/blog/faster-backups-with-sharding">wrote previously</a> on how Vitess handles backups at this scale, but what about Postgres?</p><p><a href="https://planetscale.com/neki">Neki</a> distributes Postgres data across shards so each shard can be backed up and restored independently.</p>]]></content>
    <summary><![CDATA[With backups being such a vital part of keeping your data safe, how do they actually work?]]></summary>
  </entry>
  <entry>
    <title>Making 768 servers look like 1</title>
    <link href="https://planetscale.com/blog/making-768-servers-look-like-1"/>
    <id>https://planetscale.com/blog/making-768-servers-look-like-1</id>
    <published>2026-07-15T00:00:00.000Z</published>
    <updated>2026-07-15T00:00:00.000Z</updated>
    <author>
      <name>Ben Dicken</name>
    </author>
    <category term="engineering"/>
    <category term="postgres"/>
    <content type="html"><![CDATA[<iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#servers" title="/blog/many-servers-appear-as-one/iframe#servers" loading="lazy"></iframe><p>This is 768 servers.</p><p>To some, that looks like a lot of computers. To those managing the infrastructure for apps with millions of customers, executing millions of queries per second, pretty normal. Products at this scale frequently require thousands of servers working in unison.</p><p>The most difficult infrastructure component to scale is almost always the database. A single database server cannot handle such demand, so we must spread the queries and data out across many servers with <span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">database sharding</span>.</p><p>Database sharding is the best way to scale a Postgres or MySQL database for anything beyond a few terabytes of data. Let&#x27;s look at how we go from a small single-node database, to one with a few terabytes spread across four shards, all the way up to one that is sharded across 768 servers and storing a petabyte of data.</p><h2 id="growing-pains"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#growing-pains">Growing pains</a></h2><p>To understand why sharding is a necessary part of scaling relational databases, we must understand the bottlenecks of less scalable approaches.</p><p>Consider first a simple application architecture.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#popular-arch" title="/blog/many-servers-appear-as-one/iframe#popular-arch" loading="lazy"></iframe><p>Most applications you&#x27;ve ever used function in this way, or at least did early in their existence. The software running on a client device connects to an app server over the internet. This app server lives in a data center and handles authentication, page loads, and all the server-side logic for how your application behaves. All the persisted data like user accounts, posts, settings, and messages get stored in and retrieved from the database server (where &quot;database server&quot; is typically Postgres or MySQL, though the focus of this article is Postgres).</p><p>Even with a large database servers (10s of CPU cores, 100s of gigabytes of RAM) bottlenecks arise pretty quickly. Typically, it is either CPU constraints due to high query volume, or I/O constraints (IOPS) due to a high volume of reads and writes.</p><p>This is summed up nicely by the Universal Scalability Law:</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#universal-scalability-law" title="/blog/many-servers-appear-as-one/iframe#universal-scalability-law" loading="lazy"></iframe><p>In short, the USL states that resource <em>contention</em> causes scalability to grow sub-linearly with increasing resources, and at a certain point, <em>incoherence</em> causes performance degradation. This is true for Postgres, as with any software system attempting to scale out across many threads or processes on a larger server.</p><p>One way to solve this, at least in the short term, is leveraging read-replicas.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#primary-replicas" title="/blog/many-servers-appear-as-one/iframe#primary-replicas" loading="lazy"></iframe><p>In this configuration, you maintain the original server as a <em>primary</em> and add additional <em>replicas</em> as shown above.</p><p>The primary sends a continuous stream of messages to every replica to ensure they stay up-to-date with the data changes on the primary. Writes (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>) can only go to the primary. If writes were allowed to any server, we could end up with conflicting data. Solving this requires complex and slow consensus algorithms, which is possible, but in most cases not ideal for optimal performance.</p><p>However, app servers can send read (<code>SELECT</code>) queries to the replicas. Since most apps have a much higher percent of reads compared to writes, this provides a lot more scalability. (Replicas are also necessary for high availability and data durability, even if query traffic does not require them).</p><p>The database can scale to handle more traffic by adding replicas. An extreme example of this is <a href="https://openai.com/index/scaling-postgresql/">OpenAI&#x27;s use of 50 replicas on a single Primary</a>.</p><p>It turns out, scaling servers vertically (increasing CPU / RAM) and adding replicas can only take you so far. There are several bottlenecks that cannot be solved in this way</p><h3 id="1-writes-limited-to-one-server"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#1-writes-limited-to-one-server">1) Writes limited to one server</a></h3><p>With high enough write volume, no amount of additional read-only replicas will alleviate an issue. Before Postgres can acknowledge a committed write, it must record the change in its write-ahead log (WAL) and flush that log to durable storage. The WAL is a shared resource amongst all connections on the primary. This is essentially a single write bottleneck across your entire database, even if you have tens of replicas.</p><h3 id="2-replicas-do-not-increase-data-capacity"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#2-replicas-do-not-increase-data-capacity">2) Replicas do not increase data capacity</a></h3><p>A replica is a full copy of the primary&#x27;s data, including all indexes. Adding replicas gives us more places to run reads, but it does not distribute the data.</p><h3 id="3-backups"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#3-backups">3) Backups</a></h3><p>Backups are an important part of data durability and RPO / RTO guarantees. Taking a backup of a large, monolithic database to object storage can take hours or even days due to the bandwidth limitations of node-to-storage communication. This is unacceptably long for many organizations that rely on frequent and validated backups.</p><p>The most proven way to handle this is sharding.</p><h2 id="sharding-with-a-d"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#sharding-with-a-d">Sharding, with a &quot;d&quot;</a></h2><p>Sharding solves these three bottlenecks by distributing the data and queries across many distinct primaries. For data, it is useful because a single node can only store so much and is limited on write throughput. For queries, this is useful because the network interconnects and CPUs can only process so many queries at a time.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#sharding" title="/blog/many-servers-appear-as-one/iframe#sharding" loading="lazy"></iframe><p>Sharding is useful at all scales past a few terabytes of data. For example, with 2 terabytes of data, we may choose a setup with four shards, each storing 500 gigabytes and handling 1/4th of the total query traffic. When we needed to store a petabyte of data (one million gigabytes), we&#x27;d need many more shards. In this case, we can use 256 shards, each with a primary + 2 replicas, and each responsible for storing ~4 terabytes. This requires 256 * 3 = 768 servers!</p><p>Without a good system in place, this adds significant complexity to our app&#x27;s backend. With so much going on, how does the system...</p><ul><li>Decide which data goes to which server?</li><li>Decide which queries go to which server?</li><li>Handle queries that need to talk to multiple shards simultaneously?</li><li>Take backups across this spread-out database?</li><li>Monitor system-wide health?</li><li>Respond to a failing server?</li></ul><p>There&#x27;s a lot that could be said in addressing each one of those concerns. But the question to address here in this article is the following:</p><p><span class="bg-blue-100/80 text-blue-700 dark:bg-blue-800/80 dark:text-blue-300">How can these 768 servers look like 1 cohesive database to our apps?</span></p><p>We want to allow the application servers to go from interacting with a complex system, like this:</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#tons-of-shards" title="/blog/many-servers-appear-as-one/iframe#tons-of-shards" loading="lazy"></iframe><p>To instead interacting with it over a single connection string, making it appear as if it&#x27;s interfacing with one large, scalable database:</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#simple-sharded" title="/blog/many-servers-appear-as-one/iframe#simple-sharded" loading="lazy"></iframe><p>While in reality, utilizing tens or hundreds of shards. <a href="https://planetscale.com/neki">Neki</a> for Postgres and <a href="https://vitess.io/">Vitess</a> for MySQL solve this. Let&#x27;s see how.</p><h2 id="the-proxy-layer"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#the-proxy-layer">The proxy layer</a></h2><p>The most important amongst several critical pieces here is the proxy layer.</p><p>Proxies are middleware servers that sit between two services. In our case, these two services are the application servers and database servers.</p><p>Proxies are frequently used with Postgres databases. Even when there&#x27;s no sharding, they are useful for connection pooling and request queuing. For regular (unsharded) Postgres, PgBouncer is a popular proxy that people use to multiplex 1000s of app connections across fewer direct Postgres connections.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#pgbouncer" title="/blog/many-servers-appear-as-one/iframe#pgbouncer" loading="lazy"></iframe><p>PgBouncer has a simple goal. It&#x27;s built to accept a large number of connections from many clients and route them through a smaller pool of connections that it continually maintains with Postgres. The query queuing is useful for traffic surges and during database failover, so requests can resume when the new primary comes online. We have a whole <a href="https://planetscale.com/blog/scaling-postgres-connections-with-pgbouncer">blog on PgBouncer</a> if you want to learn more.</p><p>Sharding Postgres requires an even more sophisticated proxy. The biggest difference is that, in addition to multiplexing and buffering, the proxy must understand how data is distributed across servers and route SQL queries to the correct shards. Because of this, we refer to it as a <em>router</em>.</p><p>When inserting data, the router must be aware of how data is to be distributed. This is known as the <a href="https://planetscale.com/blog/database-sharding#sharding-strategy">sharding strategy</a>.</p><p>A common approach is to shard incoming rows based on a hash of an id column. When inserting row like this into the database:</p><div class="code-block" data-language="SQL"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> INSERT INTO</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> users (id, username, email) </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">VALUES</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">1</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'ada'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'ada@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">2</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'grace'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'grace@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">3</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'linus'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'linus@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">4</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'margaret'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'margaret@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">5</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'dennis'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'dennis@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">6</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'barbara'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'barbara@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">7</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'donald'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'donald@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">),</span></span>
<span class="line"><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">    (</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082">8</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'james'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">, </span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C">'james@example.com'</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">);</span></span>
<span class="line"></span></code></pre></div></div><p>Each of the four shards is assigned a range of IDs that it&#x27;s responsible for storing, and the router sends the inserts to the correct shard. The insertions first get sent to the router, where it computes a hash of each ID, then forwards it along to the correct shard.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#shard-inserts" title="/blog/many-servers-appear-as-one/iframe#shard-inserts" loading="lazy"></iframe><p>When it comes to reads, some queries are simple enough such that the router passes them along to a single shard.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SELECT</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> email </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">from</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> user </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">where</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">=</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 4</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"></span></code></pre></div></div><p>In this case, all the router needs to do is have an internal mapping of which user IDs live in which servers, and forward that query on. Based on the example above, this would be the first (top) shard.</p><p>Some cases are more complex.</p><div class="code-block" data-language="sql"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">SELECT</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> email </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">FROM</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> user</span></span>
<span class="line"><span style="--shiki-light:#F35815;--shiki-dark:#F35815">  WHERE</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1"> id </span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">BETWEEN</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 3</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815"> AND</span><span style="--shiki-light:#D92038;--shiki-dark:#FF7082"> 5</span><span style="--shiki-light:#2B2B2B;--shiki-dark:#E1E1E1">;</span></span>
<span class="line"></span></code></pre></div></div><p>Users with this range of IDs are spread out across several shards. The router must understand the data topology, create a plan for distributing the query to all shards that may contain matching results, aggregate the results back at the router, and send the full result set to the client.</p><p>Ultimately, this means the router itself must have a full query parser and routing planner built in.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#proxy-plan" title="/blog/many-servers-appear-as-one/iframe#proxy-plan" loading="lazy"></iframe><p>The router must be able to perform query parsing, planning, connection pooling, and buffering, all within a single system. Complex software is hard to get right.</p><h2 id="how-does-it-know"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#how-does-it-know">How does it know?</a></h2><p>Every database is unique, with its own schema, tables, and query patterns. How then can a router generically know which data, and which queries, go where?</p><p>In both <a href="https://planetscale.com/neki">Neki</a> and <a href="https://vitess.io/docs/reference/features/vschema/">Vitess</a>, these are specified via JSON files representing the data topology of the system. Vitess&#x27; VSchema and Neki&#x27;s data topology give engineers a ton of flexibility to describe precisely how tables and queries should be distributed. Below is a simplified example of how we would specify a sharding scheme for a <code>user</code> table:</p><div class="code-block" data-language="json"><div class="min-w-0 max-w-full"><pre class="shiki shiki-themes planetscale-light planetscale-dark" style="--shiki-light:#2b2b2b;--shiki-dark:#e1e1e1;--shiki-light-bg:#ebebeb;--shiki-dark-bg:#1a1a1a" tabindex="0"><code><span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">{</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">  "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">shard_indexes</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> {</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">    "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">user_hash</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> {</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">      "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">type</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C"> "hash"</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">    }</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">  },</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">  "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">tables</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> {</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">    "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">user</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1"> {</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">      "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">shard_by</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C"> "user_hash"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">,</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">      "</span><span style="--shiki-light:#F35815;--shiki-dark:#F35815">column</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">"</span><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">:</span><span style="--shiki-light:#13862E;--shiki-dark:#75DB8C"> "id"</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">    }</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">  }</span></span>
<span class="line"><span style="--shiki-light:#616161;--shiki-dark:#C1C1C1">}</span></span>
<span class="line"></span></code></pre></div></div><p>This metadata is stored in the router, and tells it that the <code>user</code> table is sharded on its <code>id</code> column using the <code>user_hash</code> shard index. This <code>user_hash</code> shard index uses the router&#x27;s built-in value hashing. For each incoming row, it hashed the ID, and uses this to send it to the correct shard to be stored.</p><p>Since this is all communicated to the router via text and JSON, AI agents are great for configuration and optimization here.</p><h2 id="many-proxies-one-database"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#many-proxies-one-database">Many proxies, one database</a></h2><p>At a scale of 256 shards spanning 768 servers and millions of queries per second, we cannot route all of this traffic through a single proxy. We need many! Perhaps 10, perhaps 100, depending on the shape of the traffic.</p><p>We&#x27;d still like our apps to think of this as a single server. This is where a Network Load Balancer (NLB) helps.</p><p>NLBs have a simple job: Allow connections via a single host/IP, and assign each connection to one of many destinations. This is how traffic is distributed across the routers. Once assigned, a connection remains with the same proxy for its lifetime.</p><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#full-sharded" title="/blog/many-servers-appear-as-one/iframe#full-sharded" loading="lazy"></iframe><p>In some cases, an NLB is not necessary. Eliminating an NLB adds slightly more complexity to the app server&#x27;s connection logic, as it will have to be aware of each router&#x27;s host, but eliminates a network hop, keeping round-trip latency to a minimum.</p><h2 id="the-full-picture"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#the-full-picture">The full picture</a></h2><p>Now all the pieces are in place to make 768 servers storing 1,000 terabytes of data appear as a single, monolithic database to our apps.</p><ol><li>An app server is told &quot;connect to the database at <code>mydb.pscale.com</code>&quot;</li><li>A DNS lookup is performed, returning the NLB&#x27;s IP address: <code>123.152.100.4</code></li><li>The app requests to connect to the database at <code>123.152.100.4</code></li><li>This routes the connection first through the NLB, then to one of the N proxies</li><li>The app begins sending database queries, which go app -&gt; NLB (optional) -&gt; proxy -&gt; shards. The complex routing logic is hidden from the application. (NLB not pictured below, for simplicity)</li></ol><iframe src="https://planetscale.com/blog/many-servers-appear-as-one/iframe#shard-formation" title="/blog/many-servers-appear-as-one/iframe#shard-formation" loading="lazy"></iframe><p>This example shows scaling up to 1 petabyte, but sharding should begin long before this scale. The precise recommendations depend on each database&#x27;s size, schema, and QPS, but we recommend sharding Postgres and MySQL for anything beyond a few terabytes of data. That&#x27;s the point where you typically begin hitting the bottlenecks described earlier: long backups, write bottlenecks, etc. If you are facing challenges scaling relational databases, Neki and Vitess are the solutions.</p><p><a href="https://planetscale.com/vitess">Vitess</a> for MySQL has been used for over a decade to scale the world&#x27;s biggest relational databases. We have years of experience operating large, sharded databases for our customers, and are the core maintainers of the Vitess project. <a href="https://planetscale.com/neki">Neki</a> was developed by the same expert maintainers of Vitess, bringing an even more powerful sharding system to Postgres.</p><h2 id="what-about-everything-else"><a href="https://planetscale.com/blog/making-768-servers-look-like-1#what-about-everything-else">What about everything else?</a></h2><p>We&#x27;ve only scratched the surface of everything sharding systems like Neki and Vitess provide. There are so many other interesting details. What&#x27;s the best way to shard data? How do sharded databases handle failures? How do you change the number of shards? How do you take backups across 256 shards at the same time?</p><p>Stay tuned for more here. Follow our <a href="https://planetscale.com/blog/feed.atom">RSS feed</a> or on <a href="https://x.com/planetscale">X</a> to stay in the loop.</p><p>Happy sharding.</p>]]></content>
    <summary><![CDATA[How to make 768 distinct Postgres servers look like 1 to your applications.]]></summary>
  </entry>
</feed>
