Handling Event Floods in Real-Time

April 16, 2026 (2mo ago)

How do you implement a dashboard that shows hundreds of invoices moving through a processing pipeline in real-time?

The obvious implementation is to subscribe to database changes and re-render whenever something updates. That works perfectly with a single invoice. In production, however, one invoice can generate a burst of updates in just a few seconds, and that simple approach quickly starts to break down.

Our invoice pipeline is particularly chatty (we use canonical logging, don't worry). As an invoice moves through processing, it records status transitions, stage updates, and progress percentage changes across seven different steps. Each of those changes becomes a Postgres row update, and Supabase Realtime streams every one of them to the browser. If every update immediately triggers a router.refresh(), a single invoice completing its journey can cause the entire page to be re-fetched more than a dozen times.

Handling event flooding

The first problem is handling a flood of events. When fifteen updates arrive within two seconds, reacting to each one individually creates unnecessary work.

Instead of responding to every event, use-invoice-list-realtime.ts responds to bursts of activity using a trailing debounce.

The hook subscribes to INSERT and UPDATE events on the invoices table, but each incoming event does nothing more than reset a timer.

As long as updates keep arriving, the timer continues getting pushed back. Only after the stream of events settles does the callback fire and trigger a refresh.

let timer: ReturnType<typeof setTimeout> | null = null;
const schedule = () => {
  if (timer) clearTimeout(timer);
  timer = setTimeout(() => {
    timer = null;
    onChangeRef.current();
  }, debounceMs);
};
 
channel.on("postgres_changes",
  { event: "UPDATE", schema: "public", table: "invoices" },
  schedule);

The result is that fifteen updates collapse into a single router.refresh() that runs 500 milliseconds after the final event arrives. From the user's perspective, the invoice simply updates. From the server's perspective, one refresh replaces fifteen.

It's a pattern that resembles interrupt coalescing in operating systems: the important signal is that something changed, not every individual change that occurred along the way.