Remove a channel
Unsubscribe one Realtime channel and drop it from the client registry.
supabase.remove_channel(channel) calls channel.unsubscribe, removes the channel from client.channels, and — when the registry is now empty — closes the underlying socket via disconnect. Use this when you're done with a single channel and want the client to potentially go idle (no socket, no heartbeat thread, no background reconnect).
If you just want to stop receiving frames on a channel but keep the socket up for other channels, call channel.unsubscribe directly — see unsubscribe.
Signature
supabase.remove_channel(channel)The same method lives on the realtime sub-client directly (supabase.realtime.remove_channel(channel)) — the top-level shortcut forwards through dispatch_realtime so it can opt into Async::Task semantics under async: true.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
channel | Supabase::Realtime::Channel | Required | A channel returned by supabase.channel(...). Must be the exact instance — topics can repeat in the flat registry, so the call removes the specific object, not all channels for the topic. |
Returns
In sync mode the call blocks until the phx_leave frame is queued and returns the result of the underlying delete (typically nil). Under async: true the realtime teardown runs in a child Async task and the call returns the Async::Task so the calling fiber doesn't stall on a blocking Socket#send. Call .wait on the task to await completion.
Example — remove a single channel
channel = supabase
.channel("public:countries")
.on_postgres_changes("*", schema: "public", table: "countries") { |p| handle(p) }
.subscribe
# ...done...
supabase.remove_channel(channel)
# Channel goes through LEAVING → CLOSED.
# If this was the last channel, the socket also disconnects.Example — remove one of several channels
The socket stays up because the registry still holds room.
orders = supabase.channel("public:orders").subscribe
room = supabase.channel("room:42").subscribe
supabase.remove_channel(orders)
# orders is removed; the socket stays open for room.Example — non-blocking teardown under async: true
Under async: true the call returns an Async::Task. .wait on it inside a reactor to await completion.
supabase = Supabase.create_client(
supabase_url: ENV.fetch("SUPABASE_URL"),
supabase_key: ENV.fetch("SUPABASE_ANON_KEY"),
async: true
)
channel = supabase.channel("public:orders").subscribe
Async do
# Returns immediately with an Async::Task; phx_leave is sent in the child task.
task = supabase.remove_channel(channel)
task.wait
endOutside a reactor, Async { } degrades to running inline, so the call matches the sync path.
Socket auto-close
remove_channel routes the eventual socket teardown through the intentional-close path (Client#disconnect), not a bare @socket.close. A bare close would fire on_close → schedule_reconnect and bring the socket right back up. With disconnect, the reconnect thread is stopped, the heartbeat thread is killed, every remaining channel state is flipped to CLOSED, and the socket stays down until the next subscribe opens it again.
`remove_channel` returns `Async::Task` under `async: true`
The realtime client is thread-based, and Socket#send is blocking; without dispatch, the calling fiber under async: true would hang for the duration of the phx_leave write. The umbrella's dispatch_realtime wraps the call in an Async block so the call returns an Async::Task the caller may .wait on. In sync mode (async: false, the default) the block runs straight through and the call returns whatever realtime.remove_channel returned (typically nil). Verified by spec/async/remove_channel_non_blocking_spec.rb.