Unsubscribe from Supabase real-time updates by calling supabase.removeChannel(channel) to disconnect a specific channel, or supabase.removeAllChannels() to disconnect all channels at once. In React, always call removeChannel inside the useEffect cleanup function so the subscription is removed when the component unmounts. Failing to clean up subscriptions causes memory leaks, duplicate event handlers, and eventually hits the concurrent connection limit on your Supabase plan.
Properly Cleaning Up Supabase Real-Time Subscriptions
Every real-time subscription in Supabase opens a WebSocket connection and registers event listeners. If you do not explicitly unsubscribe when a component unmounts or a page navigates away, those connections and listeners accumulate. This causes memory leaks, duplicate event handlers (showing the same message twice), and eventually exhausts the concurrent connection limit on your Supabase plan. This tutorial shows you the correct patterns for cleaning up subscriptions in every major framework.
Prerequisites
- A Supabase project with real-time enabled on at least one table
- @supabase/supabase-js v2+ installed
- An existing real-time subscription you want to clean up
- Basic understanding of React useEffect or equivalent lifecycle hooks
Step-by-step guide
Remove a specific channel
Remove a specific channel
When you create a channel with supabase.channel('name'), store the returned reference so you can remove it later. Call supabase.removeChannel(channel) to unsubscribe from all events on that channel and close the WebSocket connection. After removal, the channel object should not be reused — create a new one if you need to subscribe again.
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// Create and subscribe9const channel = supabase10 .channel('messages-channel')11 .on(12 'postgres_changes',13 { event: 'INSERT', schema: 'public', table: 'messages' },14 (payload) => console.log('New message:', payload.new)15 )16 .subscribe()1718// Later: unsubscribe and remove the channel19const status = await supabase.removeChannel(channel)20console.log('Channel removed:', status) // 'ok'Expected result: The channel is disconnected and no more events are received. The WebSocket connection for that channel is closed.
Remove all channels at once
Remove all channels at once
If your application has multiple subscriptions and you need to clean up all of them — for example, when the user logs out or navigates to a completely different section — use removeAllChannels(). This removes every active channel in one call, which is simpler than tracking and removing each one individually.
1// Remove every active channel2const statuses = await supabase.removeAllChannels()3console.log('All channels removed:', statuses)4// ['ok', 'ok', 'ok'] — one status per channel56// Common use case: cleanup on logout7async function handleLogout() {8 await supabase.removeAllChannels()9 await supabase.auth.signOut()10 window.location.href = '/login'11}Expected result: All real-time subscriptions are removed and their WebSocket connections are closed.
Clean up subscriptions in React with useEffect
Clean up subscriptions in React with useEffect
In React, the correct pattern is to create the subscription inside useEffect and call removeChannel in the cleanup function. React calls the cleanup function when the component unmounts or when dependencies change. This ensures no orphaned subscriptions survive after the component is gone.
1import { useEffect, useState } from 'react'23function ChatRoom({ roomId }: { roomId: string }) {4 const [messages, setMessages] = useState<any[]>([])56 useEffect(() => {7 // Create subscription8 const channel = supabase9 .channel(`room-${roomId}`)10 .on(11 'postgres_changes',12 {13 event: 'INSERT',14 schema: 'public',15 table: 'messages',16 filter: `room_id=eq.${roomId}`,17 },18 (payload) => {19 setMessages((prev) => [...prev, payload.new])20 }21 )22 .subscribe()2324 // Cleanup: remove channel on unmount or roomId change25 return () => {26 supabase.removeChannel(channel)27 }28 }, [roomId]) // Re-runs when roomId changes2930 return (31 <div>32 {messages.map((msg) => (33 <p key={msg.id}>{msg.content}</p>34 ))}35 </div>36 )37}Expected result: Navigating away from the component or changing the roomId cleanly removes the old subscription and creates a new one.
Clean up subscriptions in Vue and Svelte
Clean up subscriptions in Vue and Svelte
Vue uses onUnmounted (Composition API) or beforeDestroy (Options API) for cleanup. Svelte uses onDestroy. The pattern is the same: store the channel reference and call removeChannel in the teardown lifecycle hook.
1// Vue 3 Composition API2import { onMounted, onUnmounted, ref } from 'vue'34export function useRealtimeMessages(roomId: string) {5 const messages = ref<any[]>([])6 let channel: any = null78 onMounted(() => {9 channel = supabase10 .channel(`room-${roomId}`)11 .on('postgres_changes',12 { event: 'INSERT', schema: 'public', table: 'messages',13 filter: `room_id=eq.${roomId}` },14 (payload) => messages.value.push(payload.new)15 )16 .subscribe()17 })1819 onUnmounted(() => {20 if (channel) supabase.removeChannel(channel)21 })2223 return { messages }24}2526// Svelte27// <script>28import { onDestroy } from 'svelte'2930const channel = supabase31 .channel('messages')32 .on('postgres_changes',33 { event: 'INSERT', schema: 'public', table: 'messages' },34 (payload) => messages = [...messages, payload.new]35 )36 .subscribe()3738onDestroy(() => {39 supabase.removeChannel(channel)40})41// </script>Expected result: Subscriptions are cleaned up when Vue or Svelte components are destroyed.
Debug subscription leaks
Debug subscription leaks
If you suspect subscription leaks, check how many active channels exist. The Supabase client exposes the list of active channels. Log the count periodically during development to catch leaks early. Common signs of leaks include duplicate events (same message appearing twice), increasing memory usage, and eventually hitting the connection limit.
1// Check how many channels are active2const channels = supabase.getChannels()3console.log('Active channels:', channels.length)4console.log('Channel names:', channels.map((c) => c.topic))56// Log channel count on every navigation (for debugging)7window.addEventListener('beforeunload', () => {8 const count = supabase.getChannels().length9 if (count > 0) {10 console.warn(`Leaving page with ${count} active channels — potential leak!`)11 }12})1314// Nuclear option: remove everything15await supabase.removeAllChannels()Expected result: You can see the number of active channels and identify leaks when the count grows unexpectedly.
Complete working example
1import { useEffect, useState, useRef } from 'react'2import { createClient, RealtimeChannel } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89interface UseRealtimeOptions {10 table: string11 filter?: string12 event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*'13}1415export function useRealtime<T extends Record<string, any>>(16 { table, filter, event = '*' }: UseRealtimeOptions,17 onEvent: (payload: { eventType: string; new: T; old: T }) => void18) {19 const [status, setStatus] = useState<string>('disconnected')20 const channelRef = useRef<RealtimeChannel | null>(null)2122 useEffect(() => {23 const channelName = `${table}-${filter ?? 'all'}-${Date.now()}`24 const config: any = {25 event,26 schema: 'public',27 table,28 }29 if (filter) config.filter = filter3031 const channel = supabase32 .channel(channelName)33 .on('postgres_changes', config, (payload) => {34 onEvent(payload as any)35 })36 .subscribe((s) => setStatus(s))3738 channelRef.current = channel3940 // Cleanup on unmount or dependency change41 return () => {42 if (channelRef.current) {43 supabase.removeChannel(channelRef.current)44 channelRef.current = null45 }46 }47 }, [table, filter, event])4849 return { status }50}5152// Usage example53function MessageList({ roomId }: { roomId: string }) {54 const [messages, setMessages] = useState<any[]>([])5556 useRealtime(57 { table: 'messages', filter: `room_id=eq.${roomId}`, event: 'INSERT' },58 (payload) => {59 setMessages((prev) => [...prev, payload.new])60 }61 )6263 return (64 <ul>65 {messages.map((m) => <li key={m.id}>{m.content}</li>)}66 </ul>67 )68}Common mistakes when unsubscribing from Real-Time Updates in Supabase
Why it's a problem: Not calling removeChannel on component unmount, causing duplicate event handlers
How to avoid: Always return a cleanup function from useEffect that calls supabase.removeChannel(channel). Without it, navigating away and back creates a second subscription that fires alongside the first.
Why it's a problem: Calling channel.unsubscribe() instead of supabase.removeChannel(channel)
How to avoid: Use supabase.removeChannel(channel) to fully remove the channel from the client. channel.unsubscribe() only pauses the subscription — the channel object and WebSocket remain active.
Why it's a problem: Creating channels with the same name, causing subscription conflicts
How to avoid: Use unique channel names that include context like the table name, filter value, and a timestamp or UUID: supabase.channel(`room-${roomId}-${Date.now()}`)
Best practices
- Always store the channel reference returned by supabase.channel() so you can clean it up later
- Call supabase.removeChannel(channel) in useEffect cleanup functions in React
- Use supabase.removeAllChannels() on logout or major navigation events
- Use unique channel names to avoid conflicts between multiple subscriptions
- Check supabase.getChannels().length during development to catch subscription leaks early
- Await the removeChannel() promise if subsequent logic depends on the cleanup being complete
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a React app with Supabase real-time subscriptions. Show me the correct pattern for subscribing in useEffect and cleaning up with removeChannel when the component unmounts or when a dependency changes. Include a reusable hook.
Create a reusable React hook called useRealtime that subscribes to Supabase postgres_changes on a configurable table and filter, handles INSERT/UPDATE/DELETE events, and properly cleans up the subscription on unmount. Include channel leak detection.
Frequently asked questions
What is the difference between removeChannel() and unsubscribe()?
supabase.removeChannel(channel) fully removes the channel from the client and closes the WebSocket connection. channel.unsubscribe() pauses the subscription but keeps the channel registered — you can re-subscribe later. For component cleanup, always use removeChannel().
What happens if I forget to unsubscribe?
Orphaned subscriptions continue consuming memory and receiving events. You get duplicate event handlers, increasing memory usage, and eventually hit the concurrent connection limit on your plan (200 for Free, 500 for Pro).
Does supabase.removeAllChannels() remove broadcast and presence channels too?
Yes. removeAllChannels() removes every channel regardless of type — postgres_changes, broadcast, and presence channels are all cleaned up.
Can I re-subscribe after removing a channel?
Not to the same channel object. After removeChannel(), the channel is destroyed. Create a new channel with supabase.channel() and set up the subscription from scratch.
How many concurrent real-time connections does Supabase allow?
Free plan allows 200 concurrent connections, Pro allows 500, and Team/Enterprise plans allow more. Each channel subscription from each browser tab counts as one connection.
Should I unsubscribe when the browser tab is hidden?
Not usually. The Supabase client handles background tabs efficiently. However, if you want to reduce connection usage, you can removeChannel when the tab is hidden using the Page Visibility API and re-subscribe when it becomes visible again.
Can RapidDev help manage real-time subscriptions in my Supabase application?
Yes. RapidDev can implement proper subscription lifecycle management, create reusable hooks, and ensure your application cleans up connections to avoid memory leaks and connection limit issues.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation