DEV Community

Cover image for Neon Realtime vs Supabase Realtime
kuberdenis
kuberdenis

Posted on

Neon Realtime vs Supabase Realtime

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));
Enter fullscreen mode Exit fullscreen mode

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() or gen_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:

  1. Neon Realtime: subscribe to the SQL query.
  2. Supabase Broadcast: a trigger calls realtime.broadcast_changes() into a private channel. Supabase recommends it "for scalability and security".
  3. LISTEN/NOTIFY on Neon: a trigger sends NOTIFY, and the app re-runs the query.
  4. Supabase Postgres Changes: subscribe to row changes on players.
  5. 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
  • One Node process runs the writer and the subscriber, so both timestamps come from the same clock.
  • Write to screen is the time from sending the UPDATE until the subscriber's copy of the result contains the new score. It includes any refetch the method needs.
  • 5 warm-up writes, then 100 writes per cell with a random 250–750 ms gap. Timeout 10 s. Nothing timed out.
  • The Supabase refetches query views through supabase-js, with RLS on and a public read policy.
  • Bytes per change are the WebSocket bytes received plus the size of any refetched result.
  • One writer, one subscriber. I didn't test fan-out to many subscribers.

Write to screen

Median write-to-screen latency at 10,000 rows. Neon Realtime 40 to 44 ms on all four queries, Supabase Broadcast 58 to 125, LISTEN/NOTIFY 83 to 128, Supabase Postgres Changes 368 to 418, polling 484 to 546.

Median write-to-screen latency at 1,000,000 rows. Neon Realtime 41 to 46 ms except 179 ms on the aggregate. Supabase Broadcast 60 to 115, and 793 on the aggregate. LISTEN/NOTIFY 70 to 244. Supabase Postgres Changes 328 to 1,129. Polling 414 to 542.

The differences come down to how many trips a change takes to reach the screen:

How a change reaches the client with each method. Neon Realtime: the engine sends the changed rows. Supabase Broadcast: a trigger sends the row, then a REST refetch unless it's a filter. LISTEN/NOTIFY: a trigger sends NOTIFY, then the app re-runs the query. Supabase Postgres Changes: WAL read and RLS check, then a REST refetch unless it's a filter. Polling: the app re-runs the query every second.

  • 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.

p50 / 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

1,000,000 rows

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

Data sent to the client per change at 1,000,000 rows, in KB. Filter: Neon Realtime 0.4, Supabase Broadcast 0.3, LISTEN/NOTIFY 63.7, Supabase Postgres Changes 0.4. Join: 0.4, 71, 72.7, 71.1. Aggregate: 110.9, 48.3, 48, 48.4. Top 10: 1.8, 0.8, 0.5, 0.9.

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.

Sequence diagram. The Realtime subscriber subscribes to the suspended compute and gets the first result in 1.3 to 2.5 seconds. A LISTEN client runs LISTEN. After 5 minutes with no queries, the compute terminates the LISTEN connection and suspends, and stays suspended for the 10 minutes watched. The Realtime subscription stays live. A writer's UPDATE wakes the compute and is acknowledged in about 0.9 seconds, and the change reaches the subscriber 9 to 48 ms after the ack.

  • 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.

Top comments (2)

Collapse
 
paullovelace profile image
Paul Lovelace •

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?

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

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.