This guide adds a dedicated router group for checkout traffic to a Neki database, connects through it, and resizes it with pscale branch router update while one long-lived connection and a stream of new connections keep querying. It records what a router resize does to open connections and how long it takes.
What you will build
A second router group named checkout-api, sized independently of the default group, with a log of every query result during a resize.
application ---> router group "checkout-api" (NKR-2 x1 per zone -> NKR-5 x2 per zone)
other traffic -> router group "default" (NKR-1 x1 per zone, unchanged)
\_____ both reach the same shards
AI Prompt
Using the PlanetScale CLI (pscale 0.344.0), create a second router group on a Neki database, resize it under live traffic, and report what happened to the connections.
Requirements:
- Create the Neki database with --engine neki, --cluster-size PS-10-AWS-ARM-NEKI and --replicas 2.
- Create the group with: pscale branch router create <db> main <name> --size NKR-2 --replicas-per-cell 1. Router changes are applied asynchronously. Track them with pscale branch router list and pscale branch router changes list.
- Connect to a non-default group with pscale shell <db> main --router <group>. pscale shell refuses to run from a pipe unless PSCALE_ALLOW_NONINTERACTIVE_SHELL is set. An error raised by a query includes the serving router group as name=<group> in its CONTEXT line.
- Before changing the router, start two probes against the group: one long-lived connection that runs a query every second, and a loop that opens a new connection every few seconds. Log every result and every error.
- Read the router size from the sku.display_name field of the JSON output. The size field is not present at the top level.
- Autoscaling settings are validated. The maximum replicas per cell must be greater than replicas per cell, and the target CPU utilization must be 40, 50, 60 or 70.
- Do not claim latency isolation between router groups. That was not demonstrated.
- Run the verification step below before finishing.
Prerequisites
A PlanetScale organization with access to the Neki Platform Preview. Create an account if you do not have one, and see Neki availability and access for how an organization administrator joins the preview.
pscale0.344.0 with a service token that can create databases,psqlon the PATH (pscale shellruns it; versions 18.6 and 16.15 both produced the documented output), andjq:
export PLANETSCALE_SERVICE_TOKEN_ID="<token id>"
export PLANETSCALE_SERVICE_TOKEN="<token>"
export PLANETSCALE_ORG="<organization name>"
export PSCALE_ALLOW_NONINTERACTIVE_SHELL=1
pscale version
- A Neki database. It needs no tables for this guide:
pscale database create checkout --engine neki --org "$PLANETSCALE_ORG" \
--region us-east --cluster-size PS-10-AWS-ARM-NEKI --replicas 2 --wait
Note: Output samples are trimmed to the relevant lines. Router sizes list from NKR-0 at $6 per month to NKR-640 at $5,260 per month, and the monthly rate shown for a size covers three routers, one per availability zone. A group with two replicas per zone runs six routers. Current rates are on the Neki pricing page. Delete the database when you finish with
pscale database delete checkout --force --org "$PLANETSCALE_ORG".
1. Inspect the default router group and the available sizes
pscale branch router list checkout main --org "$PLANETSCALE_ORG"
pscale branch router sizes checkout main --org "$PLANETSCALE_ORG" | head -8
Real output, trimmed to the relevant columns. A new database starts with one default group of NKR-1 routers, one per availability zone:
NAME DEFAULT SIZE REPLICAS PER CELL AUTOSCALING STATE
default Yes NKR-1 1 No ready
NAME (10) CPU MEMORY COST
NKR-0 1/32 115 MB $6
NKR-1 1/16 128 MB $11
NKR-2 1/8 256 MB $22
NKR-5 1/4 512 MB $50
NKR-10 1/2 1 GB $90
2. Create a dedicated router group
pscale branch router create checkout main checkout-api --size NKR-2 --replicas-per-cell 1 --org "$PLANETSCALE_ORG" --format json \
| jq -c '{name, replicas_per_cell, state}'
until pscale branch router list checkout main --org "$PLANETSCALE_ORG" --format json \
| jq -e '.[] | select(.name=="checkout-api") | select(.state=="ready")' >/dev/null; do sleep 10; done
pscale branch router list checkout main --org "$PLANETSCALE_ORG"
Real output, trimmed to the relevant columns:
{"name":"checkout-api","replicas_per_cell":1,"state":"ready"}
NAME DEFAULT SIZE REPLICAS PER CELL AUTOSCALING STATE
default Yes NKR-1 1 No ready
checkout-api No NKR-2 1 No ready
The --format json output carries the size under sku.display_name. A top-level size field is not present, so jq .size returns null.
3. Connect through a specific group
An error raised by a query names the router group that served it. Run a failing query through each group:
echo "SELECT 1/0;" | pscale shell checkout main --org "$PLANETSCALE_ORG" 2>&1 | tail -2
echo "SELECT 1/0;" | pscale shell checkout main --org "$PLANETSCALE_ORG" --router checkout-api 2>&1 | tail -2
Real output:
ERROR: division by zero
CONTEXT: neki: router cell=aws_useast1b_2 uid=nkr-q07nusljagyh-default-aws-useast1b-2-c27181fb-57f9d7d775qkq7 name=default
ERROR: division by zero
CONTEXT: neki: router cell=aws_useast1c_2 uid=nkr-q07nusljagyh-checkout-api-aws-useast1c-2-63da2f4c-8ddfqfzfp name=checkout-api
The name= value at the end of each CONTEXT line is the router group that served the connection. Without PSCALE_ALLOW_NONINTERACTIVE_SHELL=1, pscale shell stops with Error: pscale shell only works in interactive mode unless PSCALE_ALLOW_NONINTERACTIVE_SHELL is set.
4. Resize the group while probes are running
Start two probes against checkout-api. The first holds one connection open and runs a query every second. The second opens a new connection every few seconds. Both write to log files.
rm -f stop.flag persist.out fresh.log resize.log
( echo '\pset tuples_only on'; while [ ! -f stop.flag ]; do echo "SELECT 'ok ' || clock_timestamp()::time(0);"; sleep 1; done ) \
| pscale shell checkout main --org "$PLANETSCALE_ORG" --router checkout-api > persist.out 2>&1 &
( while [ ! -f stop.flag ]; do
r=$(printf '\\pset tuples_only on\nSELECT 1;\n' | pscale shell checkout main --org "$PLANETSCALE_ORG" --router checkout-api 2>&1 \
| grep -v -E "Waiting for role|^ *$|Tuples only" | tr '\n' ' ' | cut -c1-140)
echo "$(date -u +%T) ${r:-NO_OUTPUT}" >> fresh.log
sleep 2
done ) &
sleep 45
Apply the resize and poll until the group is ready.
echo "$(date -u +%T) requesting NKR-5, 2 replicas per cell" >> resize.log
pscale branch router update checkout main checkout-api --size NKR-5 --replicas-per-cell 2 --org "$PLANETSCALE_ORG" --format json > /dev/null
for i in $(seq 1 90); do
st=$(pscale branch router list checkout main --org "$PLANETSCALE_ORG" --format json \
| jq -r '.[] | select(.name=="checkout-api") | "\(.sku.display_name) replicas_per_cell=\(.replicas_per_cell) \(.state)"')
echo "$(date -u +%T) $st" >> resize.log
case "$st" in "NKR-5 replicas_per_cell=2 ready") break;; esac
sleep 10
done
sleep 30; touch stop.flag; sleep 8
pscale branch router changes list checkout main checkout-api --org "$PLANETSCALE_ORG"
Real output, trimmed. The change was requested at 21:12:44 and the group reported ready at 21:14:51, which is 2 minutes 7 seconds later:
21:12:44 requesting NKR-5, 2 replicas per cell
21:12:45 NKR-5 replicas_per_cell=2 applying
...
21:14:51 NKR-5 replicas_per_cell=2 ready
ID STATE CHANGES CREATED AT
p9jtb0l792ee completed Size: NKR-2 → NKR-5, Replicas per cell: 1 → 2 2026-10-12 21:12:45
5. Configure autoscaling
The settings are validated before the change is accepted:
pscale branch router update checkout main checkout-api --autoscaling --max-replicas-per-cell 2 --target-cpu-utilization 60 --org "$PLANETSCALE_ORG" --format json | jq -r '.error'
pscale branch router update checkout main checkout-api --autoscaling --max-replicas-per-cell 5 --target-cpu-utilization 55 --org "$PLANETSCALE_ORG" --format json | jq -r '.error'
pscale branch router update checkout main checkout-api --autoscaling --max-replicas-per-cell 5 --target-cpu-utilization 60 --org "$PLANETSCALE_ORG" --format json \
| jq -c '{name, replicas_per_cell, autoscaling, max_replicas_per_cell, target_cpu_utilization, state}'
Real output:
Max replicas per cell must be greater than replicas per cell
Target cpu utilization must be 40, 50, 60 or 70
{"name":"checkout-api","replicas_per_cell":2,"autoscaling":true,"max_replicas_per_cell":5,"target_cpu_utilization":60,"state":"applying"}
The first call is rejected because the group already has 2 replicas per cell, and a maximum of 2 is not greater than that. The second call is rejected because 55 is not one of the allowed targets. The third is accepted and starts another asynchronous change.
Verify the result
Confirm what each probe saw, and that the default group was not changed:
echo "persistent connection: $(grep -c -E '^ ok ' persist.out) successful queries"
grep -v -E '^ ok |^ *$|Waiting|Tuples' persist.out | sort | uniq -c | sort -rn | head -5
echo "new connections:"; cut -d' ' -f2- fresh.log | sort | uniq -c
pscale branch router list checkout main --org "$PLANETSCALE_ORG" --format json \
| jq -r '.[] | "\(.name) \(.sku.display_name) replicas_per_cell=\(.replicas_per_cell)"'
Real output from the run:
persistent connection: 108 successful queries
12 WARNING: the database system is shutting down
1 connection to server was lost
1 SSL error: unexpected eof while reading
1 Error: exit status 2
new connections:
18 1
default NKR-1 replicas_per_cell=1
checkout-api NKR-5 replicas_per_cell=2
Expected: the new-connection probe logs 1 for all or nearly all attempts, the default group is unchanged at NKR-1 with one replica per cell, and checkout-api shows the new size and replica count. The long-lived connection can end during the change, and an occasional new connection can fail with a retryable error (see Common issues). In this run it logged its last successful query at 21:13:48, about a minute after the request, then received the shutdown warnings and was closed. The query counts, timings and the moment a long-lived connection closes vary per run.
How it works
A router group is a set of router instances with its own size, replica count per availability zone, autoscaling settings, and connection target. All groups serve the same database and shards. A size or replica change creates a change record that the platform applies by replacing router instances, and the group reports applying until it finishes. Queries sent through a group show that group's name in the CONTEXT line of an error. Cluster configuration and cluster sizing describe router groups, sizes and autoscaling in full.
Common issues
Long-lived connections are closed during a resize
The connection held open by the first probe ended with WARNING: the database system is shutting down, an SSL end-of-file error, and connection to server was lost. The SSL line read SSL error: unexpected eof while reading with one psql build and SSL SYSCALL error: EOF detected with another. The router instance serving it was replaced. The probe that opened a new connection every few seconds made 18 attempts across the same period and all of them succeeded. It also succeeded on all 52 attempts in an earlier NKR-10 to NKR-20 run. In a third run of this guide, 19 of 20 attempts succeeded and one failed with Error: failed to create role: Could not create role in your Postgres cluster. Please try again., which pscale shell printed while creating its temporary credentials during the router replacement. The other attempts around it succeeded, so retry a failed connection. In an earlier run that changed a group from NKR-10 to NKR-20 with a third replica per zone, the same happened about 3 minutes 50 seconds after the request. Applications should reconnect when a connection closes, and a connection pool should retry on a lost connection.
Router changes take from about two to twelve minutes
The NKR-2 to NKR-5 change above took 2 minutes 7 seconds. The same change in an earlier run took about 1 minute 40 seconds, and a change from NKR-10 to NKR-20 with one more replica per zone took about 12 minutes. Poll pscale branch router list until the group is ready instead of waiting a fixed time. pscale branch router changes list records each change as applying and then completed.
The size is not a top-level JSON field
pscale branch router create ... --format json and router list --format json report the size in sku.display_name. Reading .size returns null, which makes a poll such as jq -r '.size' wait forever. Use .sku.display_name, as step 4 does.
Latency isolation between groups was not shown
A load test that ran eight clients reading a 300,000-row table through the default group did not change SELECT 1 latency on checkout-api in a way that could be separated from network variation. The median stayed between 72 and 78 ms on both groups with and without the load, measured from a client far from the region. This guide makes no claim about isolation under load.
Autoscaling limits count toward the cluster node budget
PlanetScale's documentation states that an autoscaling router's maximum, not its current replica count, counts toward the cluster node limit. That behavior was not tested here.
Next steps
- Isolate one customer's data on its own shard: see "Isolate a Large Tenant on Its Own Neki Shard with the PlanetScale CLI".
- Keep checkout transactions on one shard: see "Keep Ledger Transactions on One Neki Shard with the PlanetScale CLI".
- To size routers for your own traffic, see Neki cluster sizing and how to connect to a router group.
verification:
status: verified
tested_at: "2026-10-12"
product_version: "pscale 0.344.0; Neki PostgreSQL 18.6 (Platform Preview)"
command: "read persist.out and fresh.log after a router resize, and list router groups with their sku.display_name and replicas_per_cell"
expected_result: "The new-connection probe logs 1 for all or nearly all attempts (an occasional retryable role-creation error can appear), the default group is unchanged at NKR-1 with one replica per cell, and checkout-api reports the new size and replica count. A long-lived connection can close during the change. Query counts and timings vary per run."