Change Data Capture in 2026: Supabase Webhooks, Prisma Pulse, and the “Thundering Herd” Problem

async database processing
background job processing
bulk update webhooks
cdc webhooks
change data capture
database automation
database cdc
database event architecture
database event listeners
database event overload
database event queueing
database event routing
database event streaming
database migration safety
database row triggers
database sync
database triggers
database webhooks
database webhooks for background jobs
data layer webhooks
event driven architecture
handling bulk database updates
InstaWebhook
microservices webhooks
postgres change data capture
postgres event streaming
postgresql cdc
postgres webhooks
prisma ORM
prisma pulse
prisma pulse cdc
realtime database events
realtime database triggers
real time data pipeline
realtime webhooks
safe database webhook delivery
scaling database webhooks
serverless webhooks
supabase backend
supabase cdc
supabase change data capture
supabase database triggers
supabase webhooks
webhook buffer
webhook load balancing
webhook payload processing
webhook queueing
webhook rate limiting
webhook retry mechanism
webhook throttling
webhook traffic spikes
worker queue protection
Change Data Capture in 2026: Supabase Webhooks, Prisma Pulse, and the “Thundering Herd” Problem
Databases used to be passive: you wrote to them, and you queried them. Increasingly, they’re becoming active participants in application architecture — emitting a live feed of every insert, update, and delete so that other systems can react instantly instead of polling for changes.

This pattern is called Change Data Capture (CDC), and in the Postgres ecosystem two tools show up constantly: Supabase Database Webhooks and Prisma Pulse. Both let you turn row-level changes into events. Both are genuinely useful. And both share an architectural blind spot that only shows up once you run a bulk update — a problem sometimes called the “thundering herd.”

Below is a practical look at how each tool works, where the bulk-update problem comes from, and what a resilient setup looks like.

Two Paths to Real-Time Postgres
Supabase Database Webhooks
Supabase’s Database Webhooks are, under the hood, a convenience layer over Postgres triggers combined with pg_net, an extension that lets Postgres fire asynchronous HTTP requests directly from SQL. When a row is inserted, updated, or deleted, a trigger fires and pg_net sends a POST (or GET) request in the background, so the network call doesn’t block the transaction that triggered it.

The payload Supabase sends is straightforward — it tells you the event type (INSERT, UPDATE, or DELETE), the table and schema, and the new and/or old row data:

Code example
Copy code
type UpdatePayload = {
type: ‘UPDATE’
table: string
schema: string
record: TableRecord
old_record: TableRecord
}
Worth noting: the payload does not include a dedicated event ID field — it’s just the row data and metadata above. That matters for idempotency, which we’ll come back to.

Two pg_net details from Supabase’s own docs are directly relevant to the bulk-update problem discussed below: the extension is configured to reliably process up to 200 requests per second, and response data is retained for only six hours before Supabase clears it out. Both are sensible defaults for normal traffic — and both become constraints the moment you fire tens of thousands of webhooks at once.

Prisma Pulse
Prisma Pulse takes a different approach: instead of push-based HTTP webhooks, it’s a managed CDC service that lets you subscribe to database changes directly from Prisma Client, using Postgres’s write-ahead log (via logical replication) as the source of truth. Because Pulse is built on your Prisma schema, the events you receive are typed:

Code example
Copy code
const stream = await prisma.user.stream()

for await (const event of stream) {
console.log(event.action) // ‘create’ | ‘update’ | ‘delete’
}
If you rename a column, TypeScript will flag code that still references the old name — which sidesteps a common class of bug in webhook consumers that quietly assume a JSON shape that has since changed.

A status note worth flagging, since this is the kind of detail that goes stale fast: Prisma temporarily paused Pulse in early 2025 while the team reworked it based on user feedback, and as of mid-2026 that “paused, being redesigned” notice is still the message shown on the official @prisma/extension-pulse package page. Separately, Prisma’s own documentation still walks through enabling Pulse’s real-time features specifically for Prisma Postgres-hosted databases, so the picture is mixed rather than a clean “on” or “off.” If you’re evaluating Pulse for a new project, treat this as a live question rather than a settled fact — check Prisma’s current docs and changelog before you commit production architecture to it, since the offering that’s generally available may look different from what shipped in 2023–2024.

The Bulk-Update Problem
Whichever path you use, a webhook fires per row change, not per SQL statement. This is fine — even elegant — for normal traffic: a user signs up, a row is inserted, a webhook fires, a worker sends a welcome email.

It stops being fine the moment someone runs a bulk operation. Say your marketing team wants to credit every user who signed up before a certain date:

Code example
Copy code
UPDATE users
SET credit_balance = credit_balance + 10
WHERE created_at < '2025-01-01';
If that touches 50,000 rows and you have a webhook on users UPDATE events, Postgres will fire 50,000 individual HTTP requests. Given Supabase’s documented ceiling of roughly 200 requests per second on pg_net, delivering all 50,000 takes over four minutes even in the best case — and if your receiving endpoint is slow or briefly down, pg_net will keep those requests queued rather than delivering them instantly. Combined with the six-hour retention window on response data, a slow or flaky receiver can end up missing events rather than just receiving them late.

The practical failure modes downstream teams actually run into:

Third-party rate limits. CRMs, email providers, and other APIs you’re forwarding events to will start returning 429 Too Many Requests.
Resource exhaustion. A Node.js server or serverless function that does real work per request (parsing, DB lookups, calling other services) can run out of memory or hit concurrency limits under a sudden spike.
Connection pool exhaustion. If your webhook handler queries the database for more context before processing, a burst of simultaneous handlers can exhaust your connection pool and slow down unrelated queries.
Desync between systems. If some percentage of requests fail and aren’t retried indefinitely, your source-of-truth database and your downstream system (CRM, search index, cache) quietly drift apart.
This isn’t a flaw unique to Supabase or Prisma — it’s inherent to any architecture that maps “one HTTP request per row change.” Postgres can process changes far faster than any HTTP receiver can realistically absorb them.

Building a Resilience Layer
The standard fix is to stop pointing your database directly at your application and instead put a durable buffer in between: something that can absorb a burst of events immediately, then hand them off to your workers at a pace they can actually handle, with retries and a place for events to land if they can’t be delivered.

You can build this yourself with a queue (BullMQ, SQS, Inngest, etc.) sitting behind a lightweight intake endpoint. There are also purpose-built services for this specific job. InstaWebhook is one example: it’s a webhook intake and delivery service — accept the payload at a durable endpoint, queue the delivery work outside the request path, track each event through received → queued → attempted → retried → delivered/dead-lettered states, and retry failed deliveries against configurable backoff schedules. It also offers a “bring your own database” mode, where payloads are stored in a Postgres instance you control rather than on the vendor’s infrastructure — relevant if you’re moving data covered by HIPAA, SOC 2, or similar compliance requirements. Worth being clear-eyed about it: it’s a newer, smaller product in this space rather than an established incumbent, so it’s worth evaluating against your own reliability and support requirements the way you would any early-stage infrastructure vendor, alongside self-hosted queue-plus-DLQ patterns and other webhook-infrastructure providers.

Whatever you choose — self-built or vendor — the shape of the fix is the same: accept fast, queue the actual delivery, retry with backoff, and give failed events somewhere to land (a dead-letter queue) instead of vanishing.

Best Practices for CDC and Database Webhooks

  1. Make your workers idempotent — and pick a real idempotency key. Retries mean at-least-once delivery, not exactly-once, so a worker that isn’t idempotent can send a duplicate welcome email or double-apply a credit. One correction to a common assumption: Supabase’s native database webhook payload does not include a distinct event ID field — it only gives you the row’s type, table, schema, record, and old_record. A workable idempotency key is usually the row’s own primary key combined with an updated_at timestamp, or a hash of the payload, stored in Redis or a dedicated table and checked before processing. (Prisma Pulse’s stream() API, by contrast, does provide delivery guarantees and event ordering as part of the managed service, which is one of its advantages when it’s available.)

  2. Acknowledge fast, process asynchronously. Don’t do slow, synchronous work — generating a PDF, calling a flaky third-party API — inside the webhook request itself. If the sender times out waiting for a response, it may treat the request as failed and retry, and you’ll end up doing the work twice. Validate the signature, push the job to an internal queue, and return quickly.

  3. Verify signatures on every request. A webhook receiver is effectively trusting that whatever hits the endpoint really came from your database. If someone finds the URL, they could forge a payload to trigger unwanted side effects. Verify HMAC or JWT signatures before acting on anything:

Code example
Copy code
const crypto = require(‘crypto’)

function verifySignature(payload, signatureHeader, secret) {
const expectedSignature = crypto
.createHmac(‘sha256’, secret)
.update(payload)
.digest(‘hex’)

return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signatureHeader)
)
}

  1. Watch pg_net’s worker pool if you’re self-hosting. Firing thousands of requests via pg_net consumes background Postgres worker processes. If your destination is slow to respond, those connections stay open longer than expected, which is exactly the mechanism behind the bulk-update problem above. Supabase’s docs note that pg_net health can be checked directly in SQL (select pid from pg_stat_activity where backend_type ilike ‘%pg_net%’), which is a useful thing to have in a runbook if webhooks stop firing.

Conclusion
CDC — via Supabase’s pg_net-powered webhooks or Prisma’s schema-aware event streams — is a genuinely good way to make a database feel like the active center of an application rather than a passive store. But treating every row change as its own outbound HTTP request has a real ceiling, and bulk operations are exactly where that ceiling gets found the hard way. Idempotent handlers, fast acknowledgment, signature verification, and a durable buffer between your database and your application code are the difference between a real-time architecture that scales and one that quietly falls over the first time someone runs a big migration.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Product marketing learning paths 2

Related Posts