[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"$fmQaitoTKhT-jC4LJS-nglaTKmUMiUqtzWj_GOshaH1Q":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":12,"readingMinutes":13,"html":14,"related":15},"supabase-realtime","Supabase Realtime: What It Costs and Where It Breaks","Supabase Realtime in late 2026: how messages are counted and billed, the per-plan limits, the errors you hit first, and when to switch from Postgres Changes to Broadcast.","2026-10-29",[9,10,11],"supabase","realtime","postgres",false,5,"\u003Cp>Supabase Realtime is the part of Supabase that pushes changes to the browser over a WebSocket, so a chat message or a status update shows up without a refresh. It&#39;s also the feature where a small app quietly gets expensive or starts dropping connections, and almost always for the same two reasons: every listener is billed separately, and a lot of front-end code forgets to unsubscribe.\u003C/p>\n\u003Cp>Everything below is checked on October 29, 2026 against Supabase&#39;s \u003Ca href=\"https://supabase.com/docs/guides/realtime/pricing\">Realtime pricing\u003C/a>, \u003Ca href=\"https://supabase.com/docs/guides/realtime/limits\">limits\u003C/a> and \u003Ca href=\"https://supabase.com/docs/guides/platform/manage-your-usage/realtime-messages\">message usage\u003C/a> pages.\u003C/p>\n\u003Ch2>Three features under one name\u003C/h2>\n\u003Cp>Realtime is really three things that share a connection.\u003C/p>\n\u003Cp>\u003Cstrong>Postgres Changes\u003C/strong> listens to inserts, updates and deletes on your tables. It&#39;s what most people mean by &quot;Supabase realtime&quot;, and it&#39;s what AI app builders reach for by default, because it&#39;s one \u003Ccode>.on(&#39;postgres_changes&#39;, ...)\u003C/code> call and it works.\u003C/p>\n\u003Cp>\u003Cstrong>Broadcast\u003C/strong> sends any message from one client (or your server) to everyone on a channel. Cursor positions, typing indicators, &quot;the export is ready&quot;. Nothing touches your tables.\u003C/p>\n\u003Cp>\u003Cstrong>Presence\u003C/strong> keeps a shared list of who&#39;s on a channel right now. The green dot next to a name.\u003C/p>\n\u003Ch2>How you&#39;re billed\u003C/h2>\n\u003Cp>Two numbers are billed: messages and peak connections.\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Plan\u003C/th>\n\u003Cth>Messages included\u003C/th>\n\u003Cth>Peak connections included\u003C/th>\n\u003Cth>Past that\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Free\u003C/td>\n\u003Ctd>2 million\u003C/td>\n\u003Ctd>200\u003C/td>\n\u003Ctd>hard limits, no overage\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Pro / Team\u003C/td>\n\u003Ctd>5 million\u003C/td>\n\u003Ctd>500\u003C/td>\n\u003Ctd>$2.50 per million messages, $10 per 1,000 peak connections\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Cp>The detail that matters is how a message is counted. Supabase&#39;s own wording: for Postgres Changes, &quot;each database change counts as one message per client that listens to the event.&quot; For Broadcast, it&#39;s one message sent plus one per client that receives it. Usage is billed in whole packages of a million, so 5.1 million on Pro rounds up to one extra package.\u003C/p>\n\u003Cp>So the count isn&#39;t &quot;how many rows changed&quot;. It&#39;s rows changed multiplied by everyone listening.\u003C/p>\n\u003Cimg src=\"/blog/supabase-realtime/realtime-message-fanout.webp\" alt=\"One insert fanned out to 300 open tabs counts as 300 Realtime messages; filtered to one room of 5 listeners it counts as 5\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Cp>A made-up but ordinary example. A support inbox with 300 people online during the day, each with a tab subscribed to the whole \u003Ccode>messages\u003C/code> table, and 50,000 new messages a month. That&#39;s 50,000 times 300, 15 million Realtime messages. On Pro you pay for 10 million over the quota: $25 a month, on top of everything else.\u003C/p>\n\u003Cp>Add a filter so each tab only listens to its own conversation (\u003Ccode>filter: &#39;conversation_id=eq.&#39; + id\u003C/code>), and if a conversation has 5 people watching on average, the same month is 250,000 messages. That&#39;s well inside the 5 million you already pay for. Same app, same traffic, one line of code.\u003C/p>\n\u003Cp>My rule is boring: never subscribe to a whole table from the browser unless the table is tiny and everyone really needs every row.\u003C/p>\n\u003Ch2>The limits you&#39;ll hit first\u003C/h2>\n\u003Cp>Cost is the slow problem. Limits are the loud one. These are per project, per plan:\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C/th>\n\u003Cth>Free\u003C/th>\n\u003Cth>Pro\u003C/th>\n\u003Cth>Pro, spend cap off\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Concurrent connections\u003C/td>\n\u003Ctd>200\u003C/td>\n\u003Ctd>500\u003C/td>\n\u003Ctd>10,000\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Messages per second\u003C/td>\n\u003Ctd>100\u003C/td>\n\u003Ctd>500\u003C/td>\n\u003Ctd>2,500\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Channel joins per second\u003C/td>\n\u003Ctd>100\u003C/td>\n\u003Ctd>500\u003C/td>\n\u003Ctd>2,500\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Channels per connection\u003C/td>\n\u003Ctd>100\u003C/td>\n\u003Ctd>100\u003C/td>\n\u003Ctd>100\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Cp>Notice the jump from 500 to 10,000 connections when you turn the spend cap off on Pro. A lot of &quot;Realtime stopped working at launch&quot; stories are a Pro project with the cap still on.\u003C/p>\n\u003Cp>The errors show up in the WebSocket frames (Network tab, filter by WS) and in the Realtime logs in the dashboard:\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>too_many_channels\u003C/code>: one connection joined more than 100 channels.\u003C/li>\n\u003Cli>\u003Ccode>too_many_connections\u003C/code>: the project ran out of concurrent connections.\u003C/li>\n\u003Cli>\u003Ccode>too_many_joins\u003C/code>: too many channel joins in one second.\u003C/li>\n\u003Cli>\u003Ccode>tenant_events\u003C/code>: you went over messages per second, and Supabase disconnects clients until it drops. \u003Ccode>supabase-js\u003C/code> reconnects by itself, which makes this one look like random flakiness.\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Ccode>too_many_channels\u003C/code> is the one I&#39;d bet on in a React app. A component calls \u003Ccode>supabase.channel(&#39;room&#39;)\u003C/code> in a \u003Ccode>useEffect\u003C/code>, the effect runs again on every re-render or route change, and nobody calls \u003Ccode>supabase.removeChannel(channel)\u003C/code> in the cleanup. Each visit adds a channel to the same socket until it hits 100. Search the codebase for \u003Ccode>.channel(\u003C/code> and check that every one has a matching \u003Ccode>removeChannel\u003C/code> in a cleanup function. It takes ten minutes and it&#39;s usually the whole fix.\u003C/p>\n\u003Ch2>Postgres Changes doesn&#39;t scale the way you&#39;d think\u003C/h2>\n\u003Cp>Postgres Changes checks every change against every subscriber&#39;s permissions. With 100 subscribers, one insert means 100 authorization checks. And the changes are processed on a single thread to keep them in order, so buying a bigger compute add-on doesn&#39;t make it much faster. The \u003Ca href=\"https://supabase.com/docs/guides/realtime/postgres-changes\">Postgres Changes docs\u003C/a> say it plainly: past roughly 3,000 concurrent subscribers on the same changes, use Broadcast instead. Broadcast sends a change once and fans it out.\u003C/p>\n\u003Cp>The way to do that without rewriting the front end much is to broadcast from the database: a trigger on the table that sends the change to a channel, and clients subscribe with Broadcast rather than Postgres Changes. It&#39;s more setup (channel names, a private channel with an RLS policy on \u003Ccode>realtime.messages\u003C/code>), but the work stops growing with the number of people watching.\u003C/p>\n\u003Cp>Two smaller things that bite:\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>You don&#39;t get the old row by default.\u003C/strong> For UPDATE and DELETE payloads with the previous values, the table needs \u003Ccode>alter table messages replica identity full;\u003C/code>. Without it, a delete arrives with little more than the primary key, and you can&#39;t filter DELETE events at all.\u003C/li>\n\u003Cli>\u003Cstrong>RLS isn&#39;t applied to DELETE events.\u003C/strong> Updates and inserts respect your select policies per subscriber. Deletes don&#39;t, so don&#39;t put anything sensitive in a design that relies on hiding which rows got deleted.\u003C/li>\n\u003C/ul>\n\u003Cp>And the thing every first Realtime setup forgets: a table only sends changes if it&#39;s in the \u003Ccode>supabase_realtime\u003C/code> publication. In the dashboard that&#39;s a toggle under Publications; in SQL it&#39;s \u003Ccode>alter publication supabase_realtime add table messages;\u003C/code>. If your subscription says SUBSCRIBED and nothing ever arrives, it&#39;s this.\u003C/p>\n\u003Ch2>What I&#39;d actually do\u003C/h2>\n\u003Cp>For a small app: Postgres Changes with a filter on every subscription, one channel per screen, cleaned up on unmount. Check the Realtime numbers on the usage page once a month.\u003C/p>\n\u003Cp>For anything with a few hundred people online at once, or a feed many people watch: Broadcast for the hot paths (chat, live counters, presence), Postgres Changes only for low-traffic admin screens. And turn the spend cap off on purpose, with a budget alert, rather than finding out from your users that the 500th connection was refused.\u003C/p>\n\u003Cp>A lot of the Supabase projects with these problems right now were generated by Lovable, where Realtime gets wired in early and nobody looks at it again until the bill or the errors arrive. If that&#39;s your app, my agency \u003Ca href=\"https://lovable2live.com/\">Lovable 2 Live\u003C/a> does exactly this kind of cleanup. For anything else, \u003Ca href=\"https://calendly.com/director-norsky/meeting\">book a free call\u003C/a>. All consultation is free.\u003C/p>\n",[16,20,24],{"slug":17,"title":18,"description":19},"supabase-cli","Supabase CLI: Migrations, Types and the Docker Catch","The Supabase CLI in late 2026: how to install it, why local dev needs a container runtime, and the migration commands that keep production in sync.",{"slug":21,"title":22,"description":23},"supabase-rls","Supabase RLS: What It Actually Protects","Supabase row level security explained for 2026: grants vs policies, the publishable key, why UPDATE needs a SELECT policy, views that leak, and fast policies.",{"slug":25,"title":26,"description":27},"self-host-supabase","Self-Host Supabase: What You Take On","How to self-host Supabase with Docker in 2026: server size, the one-command setup, what you lose (PITR, branching, backups) and when it's worth it.",1794181975346]