realtime icon indicating copy to clipboard operation
realtime copied to clipboard

When subscribing to a Postgres changes, if multiple channels are subscribed to a given filtered change, only one of them gets the message

Open icopp opened this issue 3 months ago • 3 comments

Bug report

  • [x] I confirm this is a bug with Supabase, not with my own application.
  • [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

We've been encountering a lot of inconsistent race condition-like behavior around Realtime Postgres changes, and after digging through the websockets messages for a while I think I've found out the reason: When multiple subscriptions with different channel names should both receive a message for the same table row, only the most recently created subscription does.

This seems to only be an issue in the hosted environments. In local dev, every channel gets the message, as expected.

To Reproduce

  • Set up multiple Realtime Postgres changes subscriptions to the same table, with different channel names and different but overlapping filters (i.e. column_a=in.(A,B,C), column_a=in.(B,C,D), column_a=in.(D,E,F)).
  • Make a change in that table that should trigger multiple subscriptions (i.e., update a row that has column_a set to D).
  • See that on the hosted environments, only one channel gets the websocket update message.

Expected behavior

All channels with subscriptions get the message.

Screenshots

Local dev:

Image

Hosted Supabase environment:

Image

This is with identical table changes in the same version of the web app running both places.

System information

  • OS: n/a
  • Browser (if applies): Chrome 140.0.7339.80
  • Version of supabase-js: 2.56.0
  • Version of Node.js: 22.x (Vercel)

icopp avatar Sep 06 '25 03:09 icopp

Can you check which version of realtime-js is being used? I'm using the latest in the example below: 2.15.5

I've just created this deno script to try to replicate the issue but I always get events on both channels as expected.

It's setting up two channels with overlapping filters (assuming I understood well what you described). I then update a row with name=blue

import { RealtimeClient } from "npm:@supabase/[email protected]";

const http_url = "wss://REDACTED.supabase.co/realtime/v1";
const api_key = "REDACTED"

const client = new RealtimeClient(http_url, { params: { apikey: api_key } });

const channel1 = client.channel("channel1")
const channel2 = client.channel("channel2")

channel1.on('postgres_changes', {
  event: '*',
  schema: 'public',
  table: 'test',
  filter: 'name=in.(red, blue, brown)',
}, (payload) => {
  console.log('Change arrived on channel1: ', payload)
})

channel2.on('postgres_changes', {
  event: '*',
  schema: 'public',
  table: 'test',
  filter: 'name=in.(black, blue, yellow)',
}, (payload) => {
  console.log('Change arrived on channel2: ', payload)
})

channel1.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    console.log('Ready to receive database changes! for filter name=in.(red, blue, brown)')
  }
  console.log(status)
})

channel2.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    console.log('Ready to receive database changes! for filter name=in.(black, blue, yellow)')
  }
  console.log(status)
})

console.log("Waiting")

This is the output:

❯ deno run --allow-read --allow-net  --allow-env=WS_NO_BUFFER_UTIL issue-1524.ts 

Waiting
Ready to receive database changes! for filter name=in.(red, blue, brown)
SUBSCRIBED
Ready to receive database changes! for filter name=in.(black, blue, yellow)
SUBSCRIBED
Change arrived on channel1:  {
  schema: "public",
  table: "test",
  commit_timestamp: "2025-09-10T04:13:31.320Z",
  eventType: "UPDATE",
  new: {
    created_at: "2025-09-10T04:07:24.369955+00:00",
    id: 2,
    name: "blue",
    text: "Blue text has been updated"
  },
  old: { id: 2 },
  errors: null
}
Change arrived on channel2:  {
  schema: "public",
  table: "test",
  commit_timestamp: "2025-09-10T04:13:31.320Z",
  eventType: "UPDATE",
  new: {
    created_at: "2025-09-10T04:07:24.369955+00:00",
    id: 2,
    name: "blue",
    text: "Blue text has been updated"
  },
  old: { id: 2 },
  errors: null
}

edgurgel avatar Sep 10 '25 04:09 edgurgel

Hi Eduardo – Boris here, I work on the same team as @icopp. Our realtime-js version is 2.15.1, according to node_modules/@supabase/supabase-js/package.json.

Also possibly worth noting (though likely irrelevant) is that we don't use RealtimeClient directly as you've done in your example, but rather only through @supabase/ssr's createBrowserClient; i.e.:

const supabase = createBrowserClient(/* some params */);
supabase.channel(/* unique id here */).on(REALTIME_LISTEN_TYPES.POSTGRES_CHANGES, /* options here*/);

borispetrovdev avatar Sep 12 '25 17:09 borispetrovdev

Yeah the supabase client just uses the realtime client under the hood I don't see how this could happen.

Hmm the version used is not too old so probably not the issue.

Would you be able to run this deno script against your project with the appropriate table/filters and see if it can be replicated? I'm trying to find a reproducible code so that we can identify the actual issue.

edgurgel avatar Sep 15 '25 07:09 edgurgel