When Supabase Realtime subscriptions connect but deliver no events, the problem is almost always one of four things: the table is not added to the supabase_realtime publication, there is no SELECT RLS policy for the subscribing user, the supabase_realtime role lacks SELECT permission on the table, or the WebSocket connection is being throttled by the browser. Walk through this checklist to identify and fix the issue.
Debugging Supabase Realtime Subscriptions That Are Not Working
Supabase Realtime uses PostgreSQL's replication system to stream database changes over WebSockets. When you call .subscribe(), the connection may report SUBSCRIBED status, but no events ever arrive. This is frustrating because there are no visible errors. This tutorial walks you through the systematic checklist for diagnosing and fixing silent Realtime failures, from publication configuration to RLS policies to browser-level issues.
Prerequisites
- A Supabase project with at least one table you want to subscribe to
- The Supabase JS client installed and initialized
- Basic understanding of RLS policies
- Access to the Supabase Dashboard SQL Editor
Step-by-step guide
Verify the table is added to the Realtime publication
Verify the table is added to the Realtime publication
Supabase Realtime only streams changes for tables explicitly added to the supabase_realtime publication. This is the most common cause of missing events. Open the Supabase Dashboard SQL Editor and run the query below to check which tables are included. If your table is not listed, add it with the ALTER PUBLICATION command.
1-- Check which tables are in the realtime publication2SELECT * FROM pg_publication_tables3WHERE pubname = 'supabase_realtime';45-- Add your table to the publication6ALTER PUBLICATION supabase_realtime ADD TABLE messages;78-- You can also add multiple tables at once9ALTER PUBLICATION supabase_realtime ADD TABLE messages, comments, notifications;Expected result: Your table appears in the pg_publication_tables query results. Changes to the table will now be streamed.
Check that a SELECT RLS policy exists for the subscribing role
Check that a SELECT RLS policy exists for the subscribing role
Realtime checks SELECT permission before delivering events to a subscriber. If RLS is enabled on the table but there is no SELECT policy for the authenticated role, events are silently dropped — the subscription reports SUBSCRIBED but no data arrives. This is the second most common cause of Realtime failures. Run the query below to check existing policies, then create a SELECT policy if none exists.
1-- View all RLS policies on your table2SELECT policyname, cmd, qual, with_check3FROM pg_policies4WHERE tablename = 'messages';56-- Create a SELECT policy for authenticated users7CREATE POLICY "Users can read their own messages"8ON messages FOR SELECT9TO authenticated10USING ((SELECT auth.uid()) = user_id);1112-- Or allow all authenticated users to read all messages13CREATE POLICY "Authenticated users can read all messages"14ON messages FOR SELECT15TO authenticated16USING (true);Expected result: A SELECT policy exists for the authenticated role on your table. Realtime events now pass the RLS check and reach subscribers.
Grant SELECT permission to the supabase_realtime role
Grant SELECT permission to the supabase_realtime role
Beyond RLS policies, the supabase_realtime PostgreSQL role needs explicit SELECT permission on your table. Tables created through the Dashboard or standard migrations usually have this grant automatically, but tables created via Prisma, raw SQL, or other tools may not. Run the GRANT command to ensure the role has access.
1-- Grant SELECT to the supabase_realtime role2GRANT SELECT ON messages TO supabase_realtime;34-- Verify existing grants5SELECT grantee, privilege_type6FROM information_schema.role_table_grants7WHERE table_name = 'messages';Expected result: The supabase_realtime role has SELECT permission on your table, allowing the replication system to read and stream changes.
Verify your client-side subscription code
Verify your client-side subscription code
Make sure your subscription code matches the correct channel and event syntax. Common mistakes include subscribing to the wrong table name, not handling the subscription status callback, or creating the subscription before the component mounts. The channel name is arbitrary, but the postgres_changes filter must specify the correct schema and table.
1import { createClient } from '@supabase/supabase-js';23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6);78// Subscribe to all changes on the messages table9const channel = supabase10 .channel('messages-changes')11 .on(12 'postgres_changes',13 { event: '*', schema: 'public', table: 'messages' },14 (payload) => {15 console.log('Change received:', payload);16 }17 )18 .subscribe((status) => {19 console.log('Subscription status:', status);20 // Should log: SUBSCRIBED21 });Expected result: The subscription status logs SUBSCRIBED, and changes to the messages table trigger the callback with the payload.
Diagnose browser-level WebSocket issues
Diagnose browser-level WebSocket issues
Browsers throttle WebSocket heartbeats when tabs are in the background, causing silent disconnections. If Realtime works when the tab is active but stops when you switch tabs, this is the cause. You can mitigate this by setting the worker option to offload heartbeats to a Web Worker, or by refetching data when the tab regains focus using the visibilitychange event.
1// Option 1: Use Web Worker for heartbeats (prevents background throttling)2const supabase = createClient(3 process.env.NEXT_PUBLIC_SUPABASE_URL!,4 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,5 {6 realtime: {7 worker: true,8 },9 }10);1112// Option 2: Refetch data when tab becomes visible again13document.addEventListener('visibilitychange', () => {14 if (document.visibilityState === 'visible') {15 // Refetch latest data to catch up on missed events16 fetchMessages();17 }18});Expected result: Realtime events are delivered reliably even when the browser tab is in the background or after switching back to the tab.
Clean up subscriptions to prevent memory leaks
Clean up subscriptions to prevent memory leaks
If you create multiple subscriptions without removing old ones, you can hit connection limits and cause unexpected behavior. Always remove channels when a component unmounts or when you no longer need the subscription. Use removeChannel for a specific channel or removeAllChannels to clear everything.
1// In a React useEffect cleanup2import { useEffect } from 'react';34useEffect(() => {5 const channel = supabase6 .channel('messages-changes')7 .on('postgres_changes',8 { event: '*', schema: 'public', table: 'messages' },9 (payload) => handleChange(payload)10 )11 .subscribe();1213 // Cleanup on unmount14 return () => {15 supabase.removeChannel(channel);16 };17}, []);Expected result: Subscriptions are properly cleaned up when components unmount, preventing memory leaks and connection limit issues.
Complete working example
1-- Supabase Realtime Debugging Checklist2-- Run these queries in the SQL Editor to diagnose Realtime issues34-- Step 1: Check if the table is in the realtime publication5SELECT schemaname, tablename6FROM pg_publication_tables7WHERE pubname = 'supabase_realtime';89-- Step 2: Add table to publication if missing10ALTER PUBLICATION supabase_realtime ADD TABLE messages;1112-- Step 3: Check RLS is enabled and policies exist13SELECT tablename, policyname, cmd, permissive, roles, qual14FROM pg_policies15WHERE schemaname = 'public' AND tablename = 'messages';1617-- Step 4: Create SELECT policy if missing18CREATE POLICY "Allow authenticated users to read messages"19ON public.messages FOR SELECT20TO authenticated21USING (true);2223-- Step 5: Check grants for supabase_realtime role24SELECT grantee, privilege_type25FROM information_schema.role_table_grants26WHERE table_name = 'messages'27 AND grantee = 'supabase_realtime';2829-- Step 6: Grant SELECT to supabase_realtime if missing30GRANT SELECT ON public.messages TO supabase_realtime;3132-- Step 7: Verify RLS is enabled on the table33SELECT relname, relrowsecurity34FROM pg_class35WHERE relname = 'messages';3637-- Step 8: Test that data can be read with the anon role38-- (This simulates what the API sees)39SET ROLE authenticated;40SELECT * FROM messages LIMIT 5;41RESET ROLE;Common mistakes when debugging Real-Time Not Working in Supabase
Why it's a problem: Assuming that subscribing to a channel means the table is automatically part of the Realtime publication
How to avoid: You must explicitly add each table to the supabase_realtime publication using ALTER PUBLICATION or the Dashboard Table Editor toggle.
Why it's a problem: Having an INSERT RLS policy but no SELECT policy, which causes Realtime to silently drop events
How to avoid: Realtime requires a SELECT policy to deliver events. Add a SELECT policy for the authenticated role on any table you subscribe to.
Why it's a problem: Creating subscriptions in React components without cleaning them up on unmount
How to avoid: Always call supabase.removeChannel(channel) in the useEffect cleanup function to prevent connection leaks.
Why it's a problem: Expecting Realtime to work reliably in background browser tabs without the worker option
How to avoid: Set realtime: { worker: true } in the client options to offload heartbeats to a Web Worker, or refetch data on the visibilitychange event.
Best practices
- Always verify your table is in the supabase_realtime publication before debugging client code
- Create explicit SELECT RLS policies for every table you want to subscribe to in Realtime
- Grant SELECT permission to the supabase_realtime role for tables created outside the Dashboard
- Use the worker: true option in the Supabase client to prevent background tab disconnections
- Log the subscription status callback to confirm the WebSocket connection is established
- Clean up subscriptions with removeChannel in component cleanup functions to prevent leaks
- Use the browser DevTools Network tab to inspect WebSocket frames and verify events are flowing
- For production apps, combine Realtime with initial data fetching to avoid missing events during connection setup
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Supabase Realtime subscription connects successfully (status shows SUBSCRIBED) but I never receive any events when data changes in the table. Walk me through a systematic debugging checklist covering publication setup, RLS policies, role grants, and client-side issues.
Run this diagnostic SQL query set against my Supabase database to check if the 'messages' table is properly configured for Realtime: verify it is in the supabase_realtime publication, has SELECT RLS policies, and the supabase_realtime role has SELECT grants.
Frequently asked questions
Why does my Realtime subscription show SUBSCRIBED but I receive no events?
The three most common causes are: the table is not in the supabase_realtime publication, there is no SELECT RLS policy for the subscribing role, or the supabase_realtime PostgreSQL role lacks SELECT permission on the table. Check all three in the SQL Editor.
Does Realtime work with RLS disabled?
Yes, if RLS is disabled on the table, Realtime delivers all changes without permission checks. However, disabling RLS is not recommended for production — write proper SELECT policies instead.
Can I subscribe to changes on multiple tables with one channel?
Yes. You can chain multiple .on('postgres_changes', ...) calls on the same channel, each with a different table filter. All listeners share the same WebSocket connection.
Why do Realtime events stop when I switch browser tabs?
Browsers throttle timers and WebSocket heartbeats in background tabs. The Supabase connection drops silently. Set worker: true in the Supabase client options to offload heartbeats to a Web Worker that is not throttled.
Does DELETE event payload include the deleted row data?
By default, DELETE events only include the old record's primary key columns. To receive the full old row, you must set the table's replica identity to FULL: ALTER TABLE messages REPLICA IDENTITY FULL.
How many concurrent Realtime connections can I have?
The free plan allows up to 200 concurrent connections, the Pro plan allows 500, and higher plans allow more. Each browser tab or client instance that subscribes counts as one connection.
Can RapidDev help set up Realtime for a production application?
Yes. RapidDev can architect your Realtime implementation including proper publication configuration, RLS policies for event delivery, connection management, and fallback strategies for high-availability applications.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation