Files
supabase/apps/docs/content/troubleshooting/realtime-too-many-channels-error.mdx
T
Miranda Limonczenko 7ce4ee53ae chore(docs) Retire supa-mdx-lint (#50602)
Closes
[DOCS-1289](https://linear.app/supabase/issue/DOCS-1289/get-the-linter-to-fix-what-it-flags-or-retirereplace-the-linter)

Stacked on #50600, which points contributors at the authoring skills.
Merge that one first.

## Problem

Contributors experienced friction with the linter. They felt nickle and
dimed for tiny nits and felt detracted from the work itself. PRs would
become noisy with tiny one-word suggestions.

Additionally, our homegrown linter is not very intelligent, causing
frequent overrides.

## Solution

This removes the linter entirely in favor of directing contributors to
use SKILLS instead.

The removal entails...

- **CI.** Delete the three `docs_lint` workflows: the PR check, the
external-PR comment companion, and the nightly `--fix` bot. Drop the
stale `zizmor.yml` ignore entry for the deleted workflow.
- **Tooling.** Delete `supa-mdx-lint.config.toml` and the 14 rule files.
Drop the `lint:mdx` script and the `@supabase/supa-mdx-lint` dependency
from docs, learn, and ui-library, and regenerate the lockfile.
- **Content.** Remove the 181 directives. A separate commit carries
Prettier's reformatting of the tables and blank lines those comments had
suppressed, so the deletion commit stays readable. No prose changes.
- **Style guide.** The word list states each rule directly instead of
describing what the linter flagged. Every term survives, including the
phrase groups that mirrored `Rule004ExcludeWords`.
- **Skills.** `write-the-docs`, `edit-the-docs`, and `review-the-docs`
drop `pnpm lint:mdx` from their self-review commands and check the word
list directly. `ask-the-docs`'s CI reference drops both workflows.

## Manual testing

1. Run `git grep -i supa-mdx-lint -- . ':!pnpm-lock.yaml'`. No matches.
2. Run `pnpm install --frozen-lockfile --lockfile-only`. It passes, so
the lockfile matches the three trimmed manifests.
3. Run `git diff master...HEAD --name-only --diff-filter=ACMR | grep -E
'\.(md|mdx)$' | xargs npx prettier --config prettier.config.mjs
--check`. All changed markdown passes.
4. Open the [reformatted filter
table](https://docs-git-docs-retire-mdx-linter-supabase.vercel.app/docs/guides/observability/logs#filter-events)
on the preview and compare it with
[production](https://supabase.com/docs/guides/observability/logs#filter-events).
The table renders the same.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Documentation guidance now uses manual prose and terminology review
with the shared word list.
* Clarified storage configuration and common Realtime channel mistakes.
* Improved table formatting, text wrapping, and selected reference
links.
  * Updated documentation authoring and review guidance.

* **Chores**
* Retired automated MDX linting from workflows and local validation
commands.
* Removed lint-suppression markers throughout documentation without
changing instructions.
  * Added targeted documentation review guidance for pull requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 10:00:41 -07:00

235 lines
5.8 KiB
Plaintext

---
title = "Fixing the TooManyChannels Error"
date_created = "2025-11-28T00:00:00+00:00"
topics = [ "realtime" ]
keywords = [ "channels", "useEffect", "react", "memory leak", "quota", "TooManyChannels", "ChannelRateLimitReached", "unsubscribe", "cleanup" ]
database_id = "dee93cc3-0ab1-4101-8ad4-31d8682c8844"
---
## What is the TooManyChannels error?
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.
## What causes TooManyChannels errors?
The most common cause is accidentally creating channels without cleaning them up, especially in React applications. This happens when:
- Components create channels on every render without unsubscribing
- `useEffect` runs multiple times due to missing or incorrect dependencies
- Components unmount without cleaning up their channels
- Development mode in React (StrictMode) causes effects to run twice
Each 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:
```tsx
// ❌ 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:
- Creating `supabase` client inside component causes it to change on every render
- Missing `supabase` from dependencies array
- No cleanup function to unsubscribe the channel
- Each render creates a new channel that's never removed
## The correct approach
```tsx
// ✅ CORRECT - Properly manages channel lifecycle
import { createClient } from '@supabase/supabase-js'
import { useEffect } from 'react'
// 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>
}
```
## How to debug channel creation
Check how many channels your app has created:
```tsx
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:
- Channel count increasing without user action
- Same channel topics appearing multiple times
- Count going up when navigating between pages
## Best practices for channel management
### 1. Create Supabase client outside components
```tsx
// ✅ Create once at module level
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
function MyComponent() {
// Use the stable client
}
```
### 2. Always unsubscribe in cleanup
```tsx
useEffect(() => {
const channel = supabase.channel('topic').subscribe()
return () => {
channel.unsubscribe()
}
}, [])
```
### 3. Use stable channel names
```tsx
// ❌ 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])
}
```
### 4. Reuse channels when possible
The Supabase client automatically reuses channels with the same topic:
```tsx
// These return the same channel instance
const channel1 = supabase.channel('chat')
const channel2 = supabase.channel('chat') // Same as channel1
console.log(channel1 === channel2) // true
```
### 5. Handle strict mode in development
React StrictMode intentionally runs effects twice in development. Your cleanup function will handle this:
```tsx
// This works correctly even in StrictMode
useEffect(() => {
console.log('Effect running')
const channel = supabase.channel('chat').subscribe()
return () => {
console.log('Cleanup running')
channel.unsubscribe()
}
}, [])
```
### 6. Clean up on unmount for dynamic channels
If you create channels based on props:
```tsx
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
}
```
### 7. Remove all channels when disconnecting
When logging out or leaving your app:
```tsx
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>
}
```