apps/docs/content/troubleshooting/realtime-too-many-channels-error.mdx
The TooManyChannels error occurs when your application tries to create more than the allowed number of Realtime channels. When you exceed this limit, you'll see an error with the code ChannelRateLimitReached.
This limit exists to protect both your application and Supabase servers from resource exhaustion.
The most common cause is accidentally creating channels without cleaning them up, especially in React applications. This happens when:
useEffect runs multiple times due to missing or incorrect dependenciesEach time you call supabase.channel('topic').subscribe(), a new channel is created unless you properly clean it up.
Here's the most common mistake that might lead to TooManyChannels errors:
// ❌ WRONG - Creates new channel on every render
function ChatRoom() {
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
useEffect(() => {
const channel = supabase.channel('chat')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log(payload)
})
.subscribe()
// Missing cleanup!
}, []) // supabase is missing from dependencies
return <div>Chat</div>
}
Why this fails:
supabase client inside component causes it to change on every rendersupabase from dependencies array// ✅ CORRECT - Properly manages channel lifecycle
import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
// Create client outside component (singleton)
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
function ChatRoom() {
useEffect(() => {
const channel = supabase
.channel('chat')
.on('broadcast', { event: 'message' }, (payload) => {
console.log(payload)
})
.subscribe()
// Cleanup function - ALWAYS unsubscribe!
return () => {
channel.unsubscribe()
}
}, []) // Empty dependencies because supabase is stable
return <div>Chat</div>
}
Check how many channels your app has created:
import { useEffect } from 'react'
function ChannelDebugger() {
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
useEffect(() => {
const interval = setInterval(() => {
const channels = supabase.getChannels()
console.log(`Active channels: ${channels.length}`)
console.log(
'Channel topics:',
channels.map((c) => c.topic)
)
}, 2000)
return () => clearInterval(interval)
}, [supabase])
return <div>Check console for channel count</div>
}
If you see the number climbing, you have a leak. Look for:
// ✅ Create once at module level
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
function MyComponent() {
// Use the stable client
}
useEffect(() => {
const channel = supabase.channel('topic').subscribe()
return () => {
channel.unsubscribe()
}
}, [])
// ❌ WRONG - Creates new channel topic on every render
function BadExample({ userId }) {
useEffect(() => {
const channel = supabase
.channel(`user-${Math.random()}`) // Random topic!
.subscribe()
return () => {
channel.unsubscribe()
}
}, [userId])
}
// ✅ CORRECT - Predictable channel topic
function GoodExample({ userId }) {
useEffect(() => {
const channel = supabase.channel(`user-${userId}`).subscribe()
return () => {
channel.unsubscribe()
}
}, [userId])
}
The Supabase client automatically reuses channels with the same topic:
// These return the same channel instance
const channel1 = supabase.channel('chat')
const channel2 = supabase.channel('chat') // Same as channel1
console.log(channel1 === channel2) // true
React StrictMode intentionally runs effects twice in development. Your cleanup function will handle this:
// This works correctly even in StrictMode
useEffect(() => {
console.log('Effect running')
const channel = supabase.channel('chat').subscribe()
return () => {
console.log('Cleanup running')
channel.unsubscribe()
}
}, [])
If you create channels based on props:
function RoomComponent({ roomId }) {
useEffect(() => {
const channel = supabase
.channel(`room:${roomId}`)
.on('broadcast', { event: 'message' }, handleMessage)
.subscribe()
return () => {
channel.unsubscribe()
}
}, [roomId]) // Re-subscribe when roomId changes
}
When logging out or leaving your app:
function LogoutButton() {
const handleLogout = async () => {
// Clean up all channels before logout
await supabase.removeAllChannels()
// Then handle logout
await supabase.auth.signOut()
}
return <button onClick={handleLogout}>Logout</button>
}