Neon shipped Realtime on October 15, 2026. You subscribe to a SQL query, and Neon keeps the result up to date as the data changes. Neon hasn't published latency numbers yet, so I measured it.
I compared it with polling, LISTEN/NOTIFY, and both flavors of Supabase Realtime, on 10,000 and 1,000,000 rows. Everything ran on October 28, 2026 in Frankfurt.
TL;DR
Median time from UPDATE to an up-to-date result on the client, at 10k and 1M rows:
- Neon Realtime: 40–46 ms for filters, joins and top-10 queries. That's one network round trip from my laptop. The change usually arrived within 8 ms of the write's commit ack.
- Supabase Broadcast: 58–60 ms on a plain filter. Joins and top-10 need a refetch, so 108–115 ms.
- LISTEN/NOTIFY: 70–244 ms. Every change costs a second query.
- Supabase Postgres Changes: 328–434 ms, and 1.1 s for the aggregate at 1M rows.
- Polling every second: 414–546 ms, change or not.
- The large aggregate used more bandwidth. At 1M rows, Neon still had the lowest latency at 179 ms, but re-sent the full 1,000-group result: ~111 KB per change.
- Scale-to-zero still works. The compute suspended with a subscription open and stayed suspended for the 10 minutes I watched. The subscription stayed live and got the next write.
What Neon Realtime does
Your server defines the query and seals it into a token that expires after 60 seconds. The browser subscribes with that token over a WebSocket. Neon sends the full result once, then only the changes.
// Server: the query and its parameters stay on your side
import { createRealtime, rawSql } from "@neon/realtime/server";
const realtime = createRealtime({ secret: process.env.NEON_REALTIME_SECRET, db: "neondb" });
const sealed = await realtime.seal({
query: rawSql("SELECT id, name, score FROM players WHERE team_id = $1", [teamId]),
});
// send `sealed` to the browser
// Browser
import { createRealtimeClient } from "@neon/realtime/client";
const client = createRealtimeClient({ url: NEON_REALTIME_URL });
const subscription = client.subscribe(sealed);
subscription.onChange(({ data }) => render(data));
The client asks your server for a fresh token before the old one expires. The React bindings and QueryRefreshController do that for you.
Limits in the beta:
-
Queries: read-only and deterministic, so no
now()orgen_random_uuid(). Joins, aggregates,ORDER BY … LIMIT, subqueries and window functions are supported. - Tables: every table needs a primary key.
- Auth: no row-level security yet. You authorize when you seal the query.
- Regions: four AWS regions (us-east-1, us-east-2, eu-central-1, ap-southeast-1).
- Price: Realtime is free during the beta. Compute and egress are billed as usual.
The setup
- Neon: 0.25 CU (1 GB RAM), Postgres 18.6, aws-eu-central-1.
- Supabase: Nano instance (0.5 GB RAM, shared CPU), Postgres 17.11, eu-central-1.
- Client: my laptop, about 40 ms round trip from Frankfurt. That round trip is the floor for every number below.
Both are the smallest compute on their platform. The Supabase project was on the Free plan. The Neon project sat in my Launch-plan org, but Neon's Free plan runs the same 0.25 CU with the same 5-minute scale-to-zero, and the whole benchmark used 0.43 CU-hours of the 100 a month it includes.
The Postgres versions were the defaults. Neon created the project on Postgres 18. Hosted Supabase tops out at 17.11 as of this writing.
The table is players(id, name, team_id, score) with 1,000 teams. Each write is UPDATE players SET score = $1 WHERE id = 1 over an open direct connection. Then I timed how long the subscriber took to see the new score.
Five ways to keep a query live:
- Neon Realtime: subscribe to the SQL query.
-
Supabase Broadcast: a trigger calls
realtime.broadcast_changes()into a private channel. Supabase recommends it "for scalability and security". -
LISTEN/NOTIFY on Neon: a trigger sends
NOTIFY, and the app re-runs the query. -
Supabase Postgres Changes: subscribe to row changes on
players. - Polling on Neon: re-run the query every second.
The four queries:
- Filter: the players in one team. 10 rows at 10k, 1,000 rows at 1M.
-
Join: the same players joined with
teams. - Aggregate: player count and top score for every team (1,000 groups).
- Top 10: the 10 highest scores across all players.
With both Supabase methods, the client applies the changed row itself for the filter. A change event alone can't update a join, an aggregate or a top 10, so for those the client refetches the query over the REST API.
How I measured
UPDATE until the subscriber's copy of the result contains the new score. It includes any refetch the method needs.supabase-js, with RLS on and a public read policy.
Write to screen
The differences come down to how many trips a change takes to reach the screen:
- Neon Realtime: the change arrived between 2 ms before and 8 ms after the writer got its commit ack. So it's as fast as the network allows. The exception is the aggregate at 1M rows (next section).
- LISTEN/NOTIFY: the notification arrives about as fast as Realtime, then the query runs a second time. At 1M rows the filter returns 1,000 rows (64 KB), so it climbs to 159 ms.
- Supabase Broadcast: the event arrived ~57 ms after the write in every cell. The REST refetch added 50–70 ms, and ~740 ms for the aggregate at 1M rows.
- Supabase Postgres Changes: the event alone took 315–384 ms. Supabase's docs say it processes changes on a single thread and checks RLS for every subscriber, and that it "does not scale as well" as Broadcast.
- Polling: on average you wait half the interval, plus the query. It also runs 86,400 queries a day per client, even when nothing changes.
Getting the first result after subscribing took 0.4–0.8 s on Neon Realtime and 0.8–1.4 s on Supabase Broadcast, including the WebSocket handshake.
At 1–2 writes a second, the write itself took ~40 ms with every method. A live query didn't slow down the writer.
1,000,000 rowsp50 / p95 tables (ms)
10,000 rows
Method
Filter
Join
Aggregate
Top 10
Neon Realtime
40 / 56
40 / 48
44 / 51
42 / 123
Supabase Broadcast
58 / 62
110 / 122
125 / 196
108 / 125
LISTEN/NOTIFY
84 / 90
83 / 87
128 / 207
85 / 315
Supabase Postgres Changes
371 / 569
397 / 619
418 / 642
368 / 615
Polling every 1 s
484 / 724
534 / 731
546 / 700
498 / 708
Method
Filter
Join
Aggregate
Top 10
Neon Realtime
44 / 52
41 / 54
179 / 191
46 / 74
Supabase Broadcast
60 / 249
115 / 132
793 / 856
110 / 121
LISTEN/NOTIFY
159 / 176
145 / 155
244 / 269
70 / 74
Supabase Postgres Changes
328 / 540
418 / 626
1129 / 1293
434 / 633
Polling every 1 s
542 / 723
495 / 740
517 / 732
414 / 709
Bytes per change
For filters and joins, Neon sends only the changed row, ~0.4 KB per change, whatever the table size. A refetch downloads the whole result again: 64–73 KB per change at 1M rows.
The large aggregate exposes Neon's bandwidth tradeoff. At 10k rows, each change arrived as a ~0.4 KB delta. At 1M rows the engine switched to re-sending the full 1,000-group result: ~111 KB and 179 ms per change. That's more than twice the data transferred by a refetch. Neon's docs warn that "complex queries add write-processing latency, especially on larger datasets."
Top 10 re-sends the full result on every change too, but that's only 1.8 KB.
Supabase's aggregate refetch took ~740 ms after the event at 1M rows. Part of that is the Nano instance: the same query took ~200 ms on Neon's 0.25 CU. Bigger Supabase compute would help.
Scale to zero
Neon suspends a compute after 5 minutes without queries. I opened a Realtime subscription while the compute was suspended, then left it alone (twice). For comparison, I did the same with a plain LISTEN connection.
- The subscription woke the compute: first result in 1.3–2.5 s.
- The compute suspended anyway, 5.3–5.4 minutes later, with the subscription open.
- It stayed suspended. In the second run I watched for 10 more minutes. The compute never woke, though the subscription's token renewed every minute.
- The subscription stayed live. A write to the sleeping database woke it (ack in 917–965 ms, most of it connecting), and the subscriber had the change 9–48 ms after the ack.
-
LISTEN didn't block suspension either, but Neon terminated the connection after 5 min 18 s (
57P01). The registration is gone, notifications sent before you reconnect are lost, and reconnecting wakes the compute again.
So an open dashboard tab doesn't keep the database billing. Polling does, because it never stops querying.
Caveats
- One subscriber and one writer at 1–2 writes a second. Fan-out and heavy write load are untested.
- Every number includes a ~40 ms round trip from my laptop. A server in the same region would see lower numbers everywhere.
- Supabase's Nano has half the RAM of Neon's 0.25 CU and a shared CPU. That affects refetch-heavy cells, not event delivery.
- Neon Realtime is a beta. Neon says the protocol isn't stable yet.
- One run per configuration, on one day.
Which one should you use?
- Neon Realtime: live results of real queries (lists, joins, leaderboards, per-user views) on Neon. Watch large aggregates, and authorize at seal time until RLS lands.
- Supabase Broadcast: you're on Supabase and the client can apply the changed row itself.
- LISTEN/NOTIFY: any Postgres, a small result, and you're fine with a refetch per change. On Neon, reconnect after the compute suspends.
- Supabase Postgres Changes: prototypes. Supabase itself points you to Broadcast.
- Polling: when a few seconds of delay is fine and the database is awake anyway.
Sources
Checked October 28, 2026.
- Neon: Realtime launch, overview, how it works, queries, subscribing, compute lifecycle, LISTEN/NOTIFY guide
- Supabase: subscribing to database changes, Postgres Changes, changelog
- Charts: rendered from the raw results with Pele





Top comments (2)
40–46ms with the change usually landing 8ms after the commit ack is the number that matters here — that's basically one round trip, not a polling interval dressed up. The 111KB-per-change aggregate at 1M rows is the catch I'd want to feel in a real app though. Do you cap that by splitting the aggregate query, or is that just the price of keeping the full result live?
40 to 46 ms on filters, joins and top-10 is hard to ignore. The aggregate resending the full 1,000-group result is the tradeoff to watch.