# How to Unsubscribe from Real-Time Updates in Supabase

- Tool: Supabase
- Difficulty: Beginner
- Time required: 10 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Create and subscribe
const channel = supabase
  .channel('messages-channel')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'messages' },
    (payload) => console.log('New message:', payload.new)
  )
  .subscribe()

// Later: unsubscribe and remove the channel
const status = await supabase.removeChannel(channel)
console.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.

### 2. 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.

```
// Remove every active channel
const statuses = await supabase.removeAllChannels()
console.log('All channels removed:', statuses)
// ['ok', 'ok', 'ok'] — one status per channel

// Common use case: cleanup on logout
async function handleLogout() {
  await supabase.removeAllChannels()
  await supabase.auth.signOut()
  window.location.href = '/login'
}
```

**Expected result:** All real-time subscriptions are removed and their WebSocket connections are closed.

### 3. 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.

```
import { useEffect, useState } from 'react'

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<any[]>([])

  useEffect(() => {
    // Create subscription
    const channel = supabase
      .channel(`room-${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`,
        },
        (payload) => {
          setMessages((prev) => [...prev, payload.new])
        }
      )
      .subscribe()

    // Cleanup: remove channel on unmount or roomId change
    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId]) // Re-runs when roomId changes

  return (
    <div>
      {messages.map((msg) => (
        <p key={msg.id}>{msg.content}</p>
      ))}
    </div>
  )
}
```

**Expected result:** Navigating away from the component or changing the roomId cleanly removes the old subscription and creates a new one.

### 4. 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.

```
// Vue 3 Composition API
import { onMounted, onUnmounted, ref } from 'vue'

export function useRealtimeMessages(roomId: string) {
  const messages = ref<any[]>([])
  let channel: any = null

  onMounted(() => {
    channel = supabase
      .channel(`room-${roomId}`)
      .on('postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'messages',
          filter: `room_id=eq.${roomId}` },
        (payload) => messages.value.push(payload.new)
      )
      .subscribe()
  })

  onUnmounted(() => {
    if (channel) supabase.removeChannel(channel)
  })

  return { messages }
}

// Svelte
// <script>
import { onDestroy } from 'svelte'

const channel = supabase
  .channel('messages')
  .on('postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'messages' },
    (payload) => messages = [...messages, payload.new]
  )
  .subscribe()

onDestroy(() => {
  supabase.removeChannel(channel)
})
// </script>
```

**Expected result:** Subscriptions are cleaned up when Vue or Svelte components are destroyed.

### 5. 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.

```
// Check how many channels are active
const channels = supabase.getChannels()
console.log('Active channels:', channels.length)
console.log('Channel names:', channels.map((c) => c.topic))

// Log channel count on every navigation (for debugging)
window.addEventListener('beforeunload', () => {
  const count = supabase.getChannels().length
  if (count > 0) {
    console.warn(`Leaving page with ${count} active channels — potential leak!`)
  }
})

// Nuclear option: remove everything
await supabase.removeAllChannels()
```

**Expected result:** You can see the number of active channels and identify leaks when the count grows unexpectedly.

## Complete code example

File: `use-realtime-with-cleanup.ts`

```typescript
import { useEffect, useState, useRef } from 'react'
import { createClient, RealtimeChannel } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

interface UseRealtimeOptions {
  table: string
  filter?: string
  event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*'
}

export function useRealtime<T extends Record<string, any>>(
  { table, filter, event = '*' }: UseRealtimeOptions,
  onEvent: (payload: { eventType: string; new: T; old: T }) => void
) {
  const [status, setStatus] = useState<string>('disconnected')
  const channelRef = useRef<RealtimeChannel | null>(null)

  useEffect(() => {
    const channelName = `${table}-${filter ?? 'all'}-${Date.now()}`
    const config: any = {
      event,
      schema: 'public',
      table,
    }
    if (filter) config.filter = filter

    const channel = supabase
      .channel(channelName)
      .on('postgres_changes', config, (payload) => {
        onEvent(payload as any)
      })
      .subscribe((s) => setStatus(s))

    channelRef.current = channel

    // Cleanup on unmount or dependency change
    return () => {
      if (channelRef.current) {
        supabase.removeChannel(channelRef.current)
        channelRef.current = null
      }
    }
  }, [table, filter, event])

  return { status }
}

// Usage example
function MessageList({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<any[]>([])

  useRealtime(
    { table: 'messages', filter: `room_id=eq.${roomId}`, event: 'INSERT' },
    (payload) => {
      setMessages((prev) => [...prev, payload.new])
    }
  )

  return (
    <ul>
      {messages.map((m) => <li key={m.id}>{m.content}</li>)}
    </ul>
  )
}
```

## Common mistakes

- **Not calling removeChannel on component unmount, causing duplicate event handlers** — undefined Fix: 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.
- **Calling channel.unsubscribe() instead of supabase.removeChannel(channel)** — undefined Fix: 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.
- **Creating channels with the same name, causing subscription conflicts** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-unsubscribe-from-real-time-updates-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-unsubscribe-from-real-time-updates-in-supabase
