You need to know when something changes on a remote service. Two paths: poll the service repeatedly asking “anything new?” or have the service webhook you when something happens. Most teams instinctively reach for polling because it’s simpler. Most production systems eventually outgrow polling and adopt webhooks. The trade-offs are sharper than they look.
This post compares polling and webhooks at a practical level: when each makes sense, how to combine them, and the operational details that separate prototype-quality from production-quality implementations.
Definitions
Polling
Your service asks the remote service “what’s new?” on a schedule.
You → API: GET /events?since=last_check
API → You: [list of new events, possibly empty]
You → API: (5 seconds later) GET /events?since=last_check
...
Webhooks
The remote service calls a URL on your service when something happens.
API → You: POST /your-webhook-endpoint { event details }
You → API: 200 OK
The fundamental difference: who initiates communication.
Polling: The Default
Polling works because every API supports GET /something and every client can call it. No coordination, no signed agreements, no webhook URLs. Just call repeatedly.
When polling makes sense
- You only need to check occasionally (every few hours, every day). The overhead is small.
- You don’t control the remote system and it doesn’t offer webhooks.
- You need data on your schedule, not the remote’s. (e.g., daily reports.)
- The remote system is unreliable for webhooks — calls might fail and you’d need to poll anyway.
- You’re prototyping. Polling is faster to wire up than webhooks.
When polling becomes painful
- You need near-real-time updates. Polling every 5 seconds = 5-second latency floor + 17,000 requests per day per resource.
- You have many resources. Polling 1000 resources every 5 seconds = 200 requests/second.
- Rate limits. Most APIs cap polling rates. You hit the cap, you slow down, your latency degrades.
- Idle most of the time. You’re making thousands of empty calls because nothing changed.
The classic polling failure mode: an application checks 10,000 resources every 5 minutes, looking for the 1% that change. Most of the traffic is wasted; the actual signal is buried in empty responses.
Webhooks: The Push Model
Webhooks invert the direction. The remote service knows when something changes and pushes a notification to you.
When webhooks make sense
- You need low-latency event delivery. Webhooks fire in milliseconds when the event happens.
- You have many resources but few changes. Webhooks scale to “millions of resources, hundreds of events per second” naturally.
- The remote system offers webhooks with reasonable delivery guarantees.
- You can run a public-facing HTTPS endpoint to receive them.
When webhooks become painful
- Your service can’t accept inbound traffic (firewalled, behind a strict NAT).
- Webhook delivery is unreliable and the remote system doesn’t retry on failure.
- You need ordering guarantees the webhook system doesn’t provide.
- Setup overhead for one-off integration is more than polling would cost.
Combining: The Hybrid Pattern
Production systems often use both:
- Webhooks for fast notification of normal events.
- Polling as a safety net to catch missed webhook deliveries.
The pattern: receive webhook → process. Once an hour, poll to reconcile in case a webhook was dropped.
This handles the worst-case webhook failure (you missed a notification) without paying polling latency for normal flow. Stripe, Shopify, and most major API providers do exactly this internally for their own systems.
Implementing Webhook Receivers
A few production patterns:
Acknowledge fast, process async
The webhook sender expects a quick 200 OK. If you do heavy processing before responding, the sender times out and retries. Worse: you can be processing the same event multiple times.
app.post('/webhooks/stripe', async (req, res) => {
// Validate signature
const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], WEBHOOK_SECRET)
// Acknowledge immediately
res.status(200).send()
// Process asynchronously
await jobQueue.add('process-stripe-event', event)
})
Validate signatures
Every webhook system signs payloads. Validate the signature before trusting the content. Otherwise anyone who guesses your URL can fake events.
const sig = req.headers['stripe-signature']
const event = stripe.webhooks.constructEvent(rawBody, sig, WEBHOOK_SECRET)
Idempotency
Webhooks can be delivered multiple times. Your processing must be idempotent — handle the same event repeatedly without bad side effects.
const event = parseEvent(req.body)
const exists = await db.events.findOne({ id: event.id })
if (exists) return res.status(200).send() // Already processed
await processEvent(event)
await db.events.insert({ id: event.id, processedAt: new Date() })
Handle ordering carefully
Some webhook systems don’t guarantee order. Event 2 might arrive before Event 1. If order matters, include sequence numbers or timestamps and reorder on your side.
Retry policy
If your endpoint returns 5xx, most senders retry with exponential backoff. Make sure your endpoint reliably returns 2xx for events you can process — even if processing is async.
Implementing Polling
A few patterns that don’t suck:
Track last-checked
Don’t fetch all data every time. Use since parameters, ETags, or cursor-based pagination.
let lastEventId = await getLastSeenEventId()
while (true) {
const events = await api.getEvents({ after: lastEventId })
for (const event of events) {
await processEvent(event)
lastEventId = event.id
}
await setLastSeenEventId(lastEventId)
await sleep(POLL_INTERVAL_MS)
}
Backoff on empty
If polling returns nothing, slow down. Don’t hammer at the same rate.
let interval = MIN_INTERVAL
while (true) {
const events = await poll()
if (events.length > 0) {
interval = MIN_INTERVAL
await processAll(events)
} else {
interval = Math.min(interval * 2, MAX_INTERVAL)
}
await sleep(interval)
}
Long polling
Some APIs support “long polling” — the server holds the request open until it has data or until a timeout. Latency approaches webhook-like; you still control the connection.
Respect rate limits
Read the rate limit headers. Don’t poll faster than the API allows.
SSE and WebSockets: The Middle Ground
Between traditional polling and webhooks, two streaming options:
Server-Sent Events (SSE)
The client opens an HTTP connection; the server pushes events as text over that connection. One-way (server to client), built on standard HTTP.
const evt = new EventSource('https://api.example.com/events')
evt.onmessage = (e) => console.log(e.data)
Good fit when:
- You want push semantics but can’t host a public webhook endpoint.
- The events are streamed continuously, not occasional.
- You’re inside a browser context.
WebSockets
Full-duplex connection. Bidirectional messaging.
Good fit when:
- You need real-time bidirectional communication (chat, gaming).
- HTTP polling/SSE isn’t enough.
For most “remote service notifies us” use cases, webhooks are simpler and SSE/WebSockets are overkill. They make sense for chat-app-like real-time bidirectional use cases.
Cost Analysis
Concrete cost math for a typical case: 10,000 resources, ~1 update per resource per day.
Polling every 5 minutes
- Requests per day: 10,000 × 288 polls = 2,880,000
- 99%+ empty responses
- Latency: 5-minute average
Polling every 30 seconds
- Requests per day: 10,000 × 2880 = 28,800,000
- Latency: 30-second average
- Probably hits rate limits
Webhooks
- Requests per day: 10,000 (one per event)
- Latency: seconds
- No wasted requests
The cost difference is 3-4 orders of magnitude. Above a certain scale, webhooks are the only feasible option.
Webhook Reliability
Major webhook providers offer different guarantees. Read the documentation:
- Stripe — At-least-once delivery; retries with exponential backoff for ~3 days; signed payloads.
- GitHub — At-least-once; ~24 hours of retries; HMAC-signed.
- Shopify — At-most-once typically; some events are at-least-once.
- Slack — Real-time messaging API with delivery guarantees vs. webhooks with different ones.
If the provider doesn’t retry on failure, you need polling as a fallback. Always.
Security
Webhook endpoints are public URLs that anyone on the internet can call. Security considerations:
Always validate signatures
Don’t trust the payload without the sender’s signature. Otherwise anyone can post fake events.
IP allowlisting (where feasible)
Many webhook providers publish their sender IP ranges. Restrict your endpoint to those. For IP-based rules, see IP blocklists and allowlists.
Rate limiting
Even with signatures, rate-limit your webhook endpoint. A bug in the sender could flood you.
HTTPS only
Webhooks send sensitive event data. HTTPS isn’t optional.
Timeouts
Set short timeouts on your processing. If a webhook handler hangs, you exhaust connection pool.
Polling-Friendly Alternatives
A few API patterns that make polling less painful:
ETags and 304s
Server returns ETag with response. Client sends If-None-Match on next poll. If unchanged, server returns 304 (small response, no body).
Conditional polling
Server supports If-Modified-Since header. Returns 200 + data if changed; 304 if not.
Cursor pagination
Server supports ?after=cursor for incremental fetching. Client only retrieves new data.
These help polling scale beyond what a naive “fetch everything” implementation would allow.
Decision Tree
A simple guide:
- Are you prototyping? Use polling. Faster to implement.
- Do you have many resources or need low latency? Use webhooks.
- Can your service accept public HTTPS callbacks? If no, polling or SSE.
- Are webhook deliveries reliable for your provider? If unclear, add polling fallback.
- Need bidirectional? WebSockets.
- Edge case: Need real-time pricing or analytics within a browser? SSE.
TL;DR
- Polling pulls; you control the schedule, the API doesn’t need to know about you.
- Webhooks push; near-real-time, but need a public HTTPS endpoint and good signature validation.
- Hybrid (webhooks + polling fallback) is the production-quality pattern.
- For webhook receivers: acknowledge fast, process async, validate signatures, handle idempotency.
- For polling: track last-checked, back off on empty, respect rate limits, use ETags where supported.
- SSE and WebSockets fill specific niches between the two.
- At scale, webhooks are 3-4 orders of magnitude cheaper than naive polling.
The polling vs webhook decision keeps coming back as systems grow. Most start with polling; most eventually migrate to webhooks for the scaling reasons above. The mature systems don’t choose — they do both, with webhooks as the fast path and polling as the safety net. For related operational topics, see rate limiting algorithms and caching IP geolocation responses — both apply the same principles of “do less work when you can.”