Launch offer: the first 1,000 users get Settl free for a year*Claim your spot
settlbuilding in public

Stop polling. Find every place your app should use a webhook instead

If you have been vibe coding for a while, you have written a loop that asks an API every five seconds whether something is done yet. The answer is almost always no. I build Settl, an expense-splitting app, solo with Claude, and I have deleted more of those loops than I want to admit. A webhook is the fix: the server calls you, once, the moment the thing happens. Here is how to find where you need one, how to read a provider's docs for it, and the prompts that build it right.

Polling is a question you keep asking

Polling is your app sending a request on a timer: GET /jobs/42pending — sleep — again. Every call that comes back pending is wasted, and the one call that matters arrives up to a full interval late. Tighten the interval and you burn quota; loosen it and the user waits.

A webhook inverts the direction. You register a URL with the provider once. When the event happens, they send you one POST with the payload. Zero wasted calls, zero delay, and your app stops carrying a timer for someone else's state.

Where the opportunities hide

Search your own code before you search any docs. Four patterns mean a webhook is waiting:

Payments, email delivery, file processing, deployments, CI runs, AI generation jobs and shipment tracking are the usual suspects. Nearly every provider in those categories publishes webhooks.

Copy these prompts

1. Find every poll in the codebase

Search this codebase for polling. Report every place that (a) calls
setInterval, setTimeout in a loop, or a cron/scheduler around an HTTP
request, (b) reads a status/state field for the same resource id more than
once, or (c) retries with a fixed delay and no error condition.

For each, give me: file:line, the resource being polled, the interval, and
the provider or service on the other end. Change nothing. I want the list.

2. Check whether the provider offers a webhook

For each provider on that list, read its docs (paste them here if I give
them; otherwise use the installed SDK under node_modules and its types) and
tell me: does it emit webhooks for the event I am polling for? Give me the
exact event name, the payload shape, and how it authenticates the callback
(signature header, shared secret, or none). Mark anything you cannot verify
against a real file as UNVERIFIED. Do not guess event names.

3. Build the receiver correctly

Write the webhook endpoint for <provider> <event>. Requirements, in order:
1. Verify the signature BEFORE parsing the body. Reject with 401 if it fails.
2. Respond 200 within 2 seconds, then do the work — never make the provider
   wait on our processing.
3. Make it idempotent: store the event id and skip any id we have already
   handled. Providers retry; we must not double-process.
4. Log the raw payload once, redacting secrets, so I can replay it locally.
Show me the route, the verification, and a test that posts a real sample
payload with a valid and an invalid signature.

4. Retire the poll safely

Now that the webhook receiver exists, remove the polling loop at <file:line>.
Keep a fallback: a reconciliation job that runs every 15 minutes, lists any
resources still marked pending after 10 minutes, and fetches their real
status once. That catches the webhook we missed. Show me the diff for the
removal and the new reconciliation job separately.

The three things receivers get wrong

They trust the body. Anyone can POST to a URL. Verify the signature first, with the provider's secret, using their exact scheme — usually an HMAC over the raw bytes. Parse the JSON after.

They do the work before replying. Providers time out in seconds and then retry. If your handler takes eight seconds to update a ledger, you will process the same event twice. Reply 200, queue the work.

They forget events repeat. Retries, replays and provider hiccups all deliver the same event more than once. Store the event id; make the handler safe to run twice (settl.fyi/social/idempotency-safe-retries).

Testing without a public URL

Providers need to reach your machine. Use a tunnel (ngrok http 3000 or Cloudflare Tunnel) and register the tunnel URL in the provider's dashboard. Most providers ship a CLI that replays real events at you — Stripe's stripe listen --forward-to localhost:3000/webhooks/stripe is the model. Keep a folder of captured payloads and post them with curl in your tests; the signature check above is what makes that safe.

When polling is still right

Keep the poll when the provider has no webhook, when you are the only consumer and the interval is minutes not seconds, or when the state you need is your own and a webhook would just be you calling yourself. And keep a slow reconciliation poll behind every webhook, because a callback you never received leaves no trace.

Run this on your codebase

Paste this into a session, from the root of your project:

Audit this project for polling that should be a webhook.

1. Find every timer, cron or loop around an HTTP request, every repeated
   read of a status field for the same id, and every fixed-delay retry with
   no error condition. List each as file:line, resource, interval, provider.
2. For each provider, state whether it offers a webhook for that event.
   Verify against the SDK on disk or docs I paste; mark the rest UNVERIFIED.
3. Rank the list by wasted calls per day (86400 / interval seconds, per
   resource).
4. Change no code. I want the ranked list, then the one receiver I should
   build first with the four requirements: verify signature before parse,
   reply 200 in under 2s, idempotent by event id, raw payload logged once.

R096: choose the push mechanism on purpose

"Realtime" does not automatically mean WebSocket. Choose from who starts the update, how long the relationship lasts, and what must happen after a disconnect.

Mechanism Direction and lifetime Good fit Required checks
Webhook Provider server to your server, one event per request Payments, delivery events, CI, background jobs Signature, fast acknowledgement, idempotency, retry handling
WebSocket Two-way, long-lived connection Collaborative editing, games, interactive control Authentication at connect, reconnect, ordering, backpressure, connection limits
Server-Sent Events Server to browser, long-lived HTTP stream Progress, notifications, model output Reconnect cursor, proxy timeouts, authorization, event IDs
Long polling Client asks, server holds until change or timeout Compatibility fallback when streaming is unavailable Timeout, cancellation, jitter, duplicate protection
Scheduled polling Client or job asks at a slow fixed interval Reconciliation or providers with no push API Quota, freshness target, backoff, stop condition

Webhooks do not replace browser streaming. WebSockets do not make provider callbacks easier. SSE is one-way by design, which is often exactly enough. Long polling still creates requests, but avoids asking every two seconds when nothing changed.

Test the choice with duplicate, delayed, reordered, disconnected, and missed events. Write down whether ordering matters globally or only per resource. Authenticate the sender or connection, save a stable event ID, and keep a slow reconciliation job when a missed event would otherwise stay invisible.

Use this decision prompt:

For this update flow, identify sender, receiver, direction, frequency,
connection lifetime, ordering need, authentication, reconnect behaviour,
and missed-event fallback. Compare webhook, WebSocket, SSE, long polling,
and scheduled polling. Choose one and state why each alternative loses.
Do not build until the duplicate and disconnect tests are defined.

Comment SOCKET for this module.

Download the runnable transport-decision pack

The starter returns a reviewable transport decision and failure plan from fictional inputs. It does not expose a public receiver or contact a provider.

Get the next one in your inbox