A toy Neki router to understand sharded Postgres

PlanetScale announced Neki: sharded Postgres. It is a router that speaks the Postgres wire protocol and fans queries out to real Postgres shards, each one a primary plus replicas. The pitch on neki.dev is Postgres at petabyte scale with zero-downtime resharding.

I wanted to see the routing decision happen, so I built the smallest thing that shows it: a ~260-line TypeScript router that speaks the Postgres wire protocol, in front of three real Postgres shards in Docker. psql connects to it. The pg driver connects to it. It reads the shard key out of each query, routes by it, and merges partial aggregates when a query has no key. Repo: danieljohnmorris/toy-neki-router.

Client over the Postgres wire protocol; the router loads a topology file and routes by shard key to one of three Dockerized Postgres shards, or scatters to all three when no key is present.

What Neki is

From the launch post, the sharding docs, the data topology reference and neki.dev, four moving parts:

  • Each shard is real Postgres: 1 primary + 2 replicas across 3 AZs, its own WAL, its own logical replication path. No modified storage engine, so extensions and SQL behave the way Postgres behaves.
  • A router sits in the wire path. The app connects with an ordinary Postgres driver. The router carries a full Postgres query parser and a distributed planner: it parses the query, decides which shards run it, and combines results into one stream.
  • Connection pooling sidecars run next to every Postgres instance. PlanetScale’s argument for why this beats PgBouncer-in-front: Neki controls both ends of the connection, so it sizes pools from what each instance can serve instead of estimating from outside.
  • A control plane owns the topology: which tables are sharded, what the shard key is, which hash key ranges live on which shard. src/topology.ts in the repo mirrors that document’s shape deliberately, and zero-downtime resharding is the control plane rewriting it live: copy a key-range slice to new shards, catch up over replication, flip the range map without dropping a connection.

One launch detail matters more than the architecture: a Neki cluster starts unsharded. Resharding is a workflow you run against the cluster you already have, not a schema decision you make on day one. The expensive part of sharding is committing to a shard key before you are sure you need one. The router is the easy part. Neki moves that commitment from “before you build” to “when you measure the ceiling”.

The toy

Three containers on ports 5433-5435, one router on 54320. The seed loads 50k rows with deliberate skew: three tenants with 10k rows each, plus 20k tenants with a single row.

Connect with an ordinary Postgres client:

psql "postgres://postgres:postgres@localhost:54320/postgres"

Run a point lookup and the router reports what it did:

> select count(*) from events where tenant_id = 'acme'
router: tenant_id='acme' -> xxhash -> 0xbb -> shard-c

One hash of the key, one lookup in the topology’s key ranges, one connection. The query never touches shard-a or shard-b.

Now drop the shard key:

> select tenant_id, count(*) from events group by tenant_id order by 2 desc
router: no shard-key predicate -> scatter across shard-a, shard-b, shard-c
router: merged partial aggregates from 3 shards

Same table, same data: three connections instead of one, and the merge happens in the router rather than in Postgres. A query with the key touches one shard. A query without it pays a fan-out and a merge.

Point lookup: one xxhash of globex's key picks shard-b, one round trip. No shard key: the query fans out to every shard and the router merges the partial aggregates.

Where the toy ends and Neki begins

The table is what the toy leaves out:

Toy routerNeki
Regex extracts WHERE key = 'literal'Full Postgres query parser + distributed planner
Simple query protocol onlyFull wire protocol, with documented preview query limits
Merges count by handPlanner pushes down and merges arbitrary aggregates
One process, no poolingHorizontally scaled routers + sidecar connection pooling
Topology JSON is staticControl plane owns topology; resharding moves ranges live

The last row is the hard part, and it is what PlanetScale’s 118-million-QPS benchmark is advertising: 512 shards, each primary on an r8g.16xlarge, 480 routers in front, p99 of 6.06ms at the router. The number worth more than the headline is per-shard throughput: 199,925 QPS at 5 shards, 198,478 at 50 - flat within 0.8% - then 231,521 at 512, higher because they let the load generator use the remaining headroom. That is the linear-scaling claim, demonstrated.

Read what the benchmark ran, though. The caveats are stated in the post itself: single-shard point selects by primary key, primary-only shards, and no writes, joins, or cross-shard queries. Primary-only is the quiet one: a production Neki shard is 1 primary + 2 replicas across 3 AZs, so the measured topology is a third of the nodes an HA cluster of that shape would run, with none of the replication traffic. That is the correct benchmark for a routing-scales-linearly claim, and it is also the shard key’s best case, not a preview of your workload. Neki’s biggest number comes from queries that stay where the shard key puts them - the same property the toy shows with two tenants landing on two different shards.

When to shard - and when not to

The toy makes the trade concrete, so Neki’s when-to-shard guide is worth restating with it in hand.

The guide says shard when one or more of these hold. My stricter read: have all three before reaching for it.

  1. A measured ceiling vertical scale can’t fix: write throughput, or a dataset no single node can hold or back up in a sane window.
  2. Every hot query can carry the shard key. In the toy, WHERE tenant_id = '...' is one shard. Anything else is every shard. Audit access patterns before picking a key.
  3. A natural, high-cardinality key that most queries include. Tenant ID is the classic. If the hottest query joins a sharded table to a big global table, the join locality problem doesn’t go away.

Don’t shard when:

  • A bigger instance fixes it. Vertical scale on a single Postgres node is cheaper than a distributed planner.
  • Read scaling is the problem. Read replicas or a cache don’t add cross-shard merge latency to every unkeyed query.
  • Queries mostly lack the key. Sharding converts one slow query into every-query-is-a-scatter. The toy shows this in one run: the group-by paid three connections and a merge for what a single node does locally.

Sharding comes after all of these. When it is needed, needing it on real Postgres (same extensions and drivers, same SQL) is a better position than needing it on a Postgres-compatible rewrite.

The decision path in the diagram: vertical scale first. The shard path only opens when a measured ceiling meets a key every hot query carries.

Replaying this on real Neki

Neki is in platform preview. When I get a cluster, the follow-up is short: create the same events table with tenant_id as the shard key, load the same skewed seed, and compare routing behaviour - which queries stay single-shard, which scatter - against the toy’s notices. Not the digests themselves: the toy hashes with XXH64 (xxhash-wasm has no XXH3-64, which Neki specifies), so tenant_id='acme' will hash differently and likely land on a different shard. PS-10 pricing puts a 3-node arm64 Neki deployment around $41/mo ($30 compute + $11 router) in us-east-1.

Until then, docker compose up, npm run seed, and npm run demo reproduce every routing notice in this post.