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/42 — pending — 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:
setIntervalor a cron around a fetch. A timer wrapped around a request is polling by definition. Every one is a candidate.- A
statusfield you read repeatedly.pending,processing,queued— if you check it more than once for the same id, the provider almost certainly offers a callback. - "Refresh to see if it went through." Any UI copy like this is a polling loop that escaped into the product.
- Retries that exist to wait rather than to recover. A retry loop with a fixed delay and no error is a poll wearing a retry's clothes.
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.