Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Debug Real-Time Not Working in Supabase

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.

What you'll learn

  • How to verify a table is added to the supabase_realtime publication
  • How RLS policies affect Realtime event delivery
  • How to grant the supabase_realtime role access to your tables
  • How to diagnose WebSocket connection issues in the browser
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+, Realtime v2March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1-- Check which tables are in the realtime publication
2SELECT * FROM pg_publication_tables
3WHERE pubname = 'supabase_realtime';
4
5-- Add your table to the publication
6ALTER PUBLICATION supabase_realtime ADD TABLE messages;
7
8-- You can also add multiple tables at once
9ALTER 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.

2

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.

typescript
1-- View all RLS policies on your table
2SELECT policyname, cmd, qual, with_check
3FROM pg_policies
4WHERE tablename = 'messages';
5
6-- Create a SELECT policy for authenticated users
7CREATE POLICY "Users can read their own messages"
8ON messages FOR SELECT
9TO authenticated
10USING ((SELECT auth.uid()) = user_id);
11
12-- Or allow all authenticated users to read all messages
13CREATE POLICY "Authenticated users can read all messages"
14ON messages FOR SELECT
15TO authenticated
16USING (true);

Expected result: A SELECT policy exists for the authenticated role on your table. Realtime events now pass the RLS check and reach subscribers.

3

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.

typescript
1-- Grant SELECT to the supabase_realtime role
2GRANT SELECT ON messages TO supabase_realtime;
3
4-- Verify existing grants
5SELECT grantee, privilege_type
6FROM information_schema.role_table_grants
7WHERE table_name = 'messages';

Expected result: The supabase_realtime role has SELECT permission on your table, allowing the replication system to read and stream changes.

4

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.

typescript
1import { createClient } from '@supabase/supabase-js';
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6);
7
8// Subscribe to all changes on the messages table
9const channel = supabase
10 .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: SUBSCRIBED
21 });

Expected result: The subscription status logs SUBSCRIBED, and changes to the messages table trigger the callback with the payload.

5

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.

typescript
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);
11
12// Option 2: Refetch data when tab becomes visible again
13document.addEventListener('visibilitychange', () => {
14 if (document.visibilityState === 'visible') {
15 // Refetch latest data to catch up on missed events
16 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.

6

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.

typescript
1// In a React useEffect cleanup
2import { useEffect } from 'react';
3
4useEffect(() => {
5 const channel = supabase
6 .channel('messages-changes')
7 .on('postgres_changes',
8 { event: '*', schema: 'public', table: 'messages' },
9 (payload) => handleChange(payload)
10 )
11 .subscribe();
12
13 // Cleanup on unmount
14 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

realtime-debug-checklist.sql
1-- Supabase Realtime Debugging Checklist
2-- Run these queries in the SQL Editor to diagnose Realtime issues
3
4-- Step 1: Check if the table is in the realtime publication
5SELECT schemaname, tablename
6FROM pg_publication_tables
7WHERE pubname = 'supabase_realtime';
8
9-- Step 2: Add table to publication if missing
10ALTER PUBLICATION supabase_realtime ADD TABLE messages;
11
12-- Step 3: Check RLS is enabled and policies exist
13SELECT tablename, policyname, cmd, permissive, roles, qual
14FROM pg_policies
15WHERE schemaname = 'public' AND tablename = 'messages';
16
17-- Step 4: Create SELECT policy if missing
18CREATE POLICY "Allow authenticated users to read messages"
19ON public.messages FOR SELECT
20TO authenticated
21USING (true);
22
23-- Step 5: Check grants for supabase_realtime role
24SELECT grantee, privilege_type
25FROM information_schema.role_table_grants
26WHERE table_name = 'messages'
27 AND grantee = 'supabase_realtime';
28
29-- Step 6: Grant SELECT to supabase_realtime if missing
30GRANT SELECT ON public.messages TO supabase_realtime;
31
32-- Step 7: Verify RLS is enabled on the table
33SELECT relname, relrowsecurity
34FROM pg_class
35WHERE relname = 'messages';
36
37-- Step 8: Test that data can be read with the anon role
38-- (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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.