dissected.io
Lesson 20 of 29
Intermediateintermediatepollingwebhookswebsocketsreal-timenetworking12 min read

Polling vs Webhooks vs WebSockets

How does your app know when something changes on the server? You have three options: ask repeatedly (polling), have the server tell you (webhooks), or keep a persistent connection open (WebSockets). Each has radically different tradeoffs.

Polling vs Webhooks vs WebSockets

Polling

client asks on a timer

Client

waiting…

GET /updates

∅ nothing

Server

idle

Webhooks

server notifies client on event

Client

listening at /webhook

POST /webhook

Server

processing events

WebSockets

persistent bidirectional connection

Client

disconnected

Server

waiting

Polling

Polling is the simplest approach: the client asks the server for updates on a fixed timer.

Every 5 seconds:
  GET /api/notifications
  → { "new": false }

Every 5 seconds:
  GET /api/notifications
  → { "new": false }

Every 5 seconds:
  GET /api/notifications
  → { "new": true, "data": [...] }

It works everywhere. Any HTTP client can do it. No special infrastructure, no persistent connections, no protocol upgrades. Simple to build and debug.

The problem is waste. In most scenarios, the vast majority of requests come back empty. If you have 10,000 clients polling every 5 seconds, that's 2,000 requests per second hitting your server — most of them pointless. Server load scales directly with the number of clients, regardless of how much activity is actually happening. There's also inherent latency: a new event might sit for up to one polling interval before the client sees it.

Use polling when updates are infrequent, a few seconds of delay is acceptable, and simplicity is the priority. A dashboard that refreshes leaderboard data every 30 seconds is a fine use case. A live chat interface is not.

Long Polling

Long polling is an improvement that eliminates most of the empty responses.

Instead of the server responding immediately, it holds the connection open and waits until it has something to say. When new data is available, it responds. The client processes it and immediately opens another long-poll request. If nothing happens for a timeout period (say, 30 seconds), the server responds with an empty result and the client reconnects.

Client → GET /api/events?since=last_id    (server holds connection open)

[28 seconds of silence]

Server → { "event": "new_message", "data": {...} }   (responds with data)

Client immediately → GET /api/events?since=new_last_id   (reconnects)

This significantly reduces the volume of empty responses. The client only gets a response when there's actual data (or a timeout). But you're still paying HTTP overhead for each update: headers, connection setup, TLS handshake on reconnect. The server is also holding many open connections in a waiting state, which has its own memory cost.

Use long polling when you need near-real-time updates but WebSockets aren't practical — maybe your infrastructure (load balancers, proxies) doesn't handle long-lived connections well, or you're in an environment where WebSockets are blocked.

Webhooks

Webhooks flip the model entirely. Rather than the client asking the server for updates, the server calls the client when something happens.

You register a callback URL with the server. When an event occurs, the server makes an HTTP POST to that URL with the event payload. No polling. No open connections. Pure event-driven.

// POST https://yourapp.com/webhooks/stripe
{
  "id": "evt_1OqXx2LkdIwHu7ix5B2ZXYZ",
  "type": "payment_intent.succeeded",
  "created": 1712000000,
  "data": {
    "object": {
      "id": "pi_3OqXx2LkdIwHu7ix0001",
      "amount": 4999,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_abc123"
    }
  }
}

This is highly efficient — the server only fires when there's something to say, and the client only processes when a request arrives. No wasted cycles.

The constraint is that your client must be a server with a publicly reachable HTTPS endpoint. This rules out browser clients and mobile apps as direct webhook recipients. Webhooks are a server-to-server pattern. They're ideal for payment callbacks (Stripe, PayPal), source control events (GitHub, GitLab), CI/CD triggers, and any async event notification between backend services.

Webhook Reliability

Webhooks introduce a reliability problem: what happens if your server is down when an event fires?

The answer is retry with exponential backoff. Reputable webhook providers (Stripe, GitHub, Twilio) will retry failed deliveries — typically over several hours or days. Each retry waits longer than the last: 30 seconds, then 5 minutes, then 30 minutes, and so on.

Your webhook handler must respond quickly — typically within 30 seconds, often less. If it doesn't, the provider treats it as a failure and retries. The right pattern is to accept the webhook, write it to a queue, and return 200 OK immediately. Process it asynchronously.

Retries introduce another problem: the same event can be delivered more than once. Your handler must be idempotent. Use the event's unique ID to deduplicate:

def handle_webhook(event):
    if already_processed(event["id"]):
        return 200  # duplicate — ignore safely

    process_event(event)
    mark_as_processed(event["id"])
    return 200

WebSockets

WebSockets provide a persistent, full-duplex connection between client and server. Once established, either side can push a message to the other at any time — no new HTTP request needed.

The connection starts as a normal HTTP request that negotiates an upgrade to the WebSocket protocol. After the handshake, the connection stays open. Messages flow in both directions over the same TCP connection, with <1ms overhead per message after the initial setup.

Client → HTTP GET /chat (with Upgrade: websocket header)
Server → 101 Switching Protocols

// Connection is now open — bidirectional
Client → { "type": "message", "text": "Hello" }
Server → { "type": "message", "from": "alice", "text": "Hey!" }
Server → { "type": "typing", "user": "bob" }
Client → { "type": "ping" }
Server → { "type": "pong" }

The result is low latency, efficient real-time communication. Chat applications, multiplayer games, live dashboards, collaborative document editing — any scenario where you need sub-second updates flowing in either direction.

The tradeoff is operational complexity. Persistent connections mean stateful servers, which complicates horizontal scaling. You need a mechanism (like Redis pub/sub) to route messages to the right server when a user's connection lives on server A but the event originates on server B.

Server-Sent Events (SSE)

Worth knowing as a middle ground: Server-Sent Events give you a one-way persistent connection from server to client, over plain HTTP.

The client opens a long-lived GET request. The server streams newline-delimited events down the response body indefinitely. The client's browser handles reconnection automatically if the connection drops.

SSE is simpler than WebSockets when you only need server-to-client updates — stock tickers, live notifications, progress updates. Because it runs over HTTP/2, it plays nicely with load balancers and proxies that might choke on WebSocket upgrades. You don't get client-to-server push, but you often don't need it.

Comparison

PatternDirectionConnectionLatencyComplexityBest For
PollingClient → ServerNew HTTP each timeUp to poll intervalLowInfrequent updates, simple setup
Long PollingClient → Server (held open)Reconnect per eventNear real-timeMediumReal-time without WebSocket support
WebhooksServer → ClientNew HTTP per eventNear real-timeMediumServer-to-server async events
WebSocketsBidirectionalPersistent<1ms after connectHighChat, games, collaborative editing
SSEServer → ClientPersistent (HTTP)Near real-timeLow–MediumLive feeds, notifications

Real-World Examples

  • Slack, Discord — WebSockets for all messaging. Bidirectional, low-latency, persistent connections at massive scale.
  • Stripe, GitHub — Webhooks for payment events and repository activity. Server-to-server, event-driven, no polling overhead.
  • Google Docs — WebSockets for collaborative editing. Every keystroke propagates to all open editors in real time.
  • Twitter/X live timeline — SSE to stream new tweet events to the browser without a full WebSocket upgrade.
  • Internal dashboards, leaderboards — Polling every 30–60 seconds. Simple to operate, good enough when updates are infrequent.
  • CI/CD pipelines — Webhooks from GitHub to trigger builds. A push event fires once, Jenkins or GitHub Actions reacts instantly.

Key Takeaways

  • Polling is simple but wasteful — server load scales with client count regardless of actual activity
  • Long polling reduces empty responses but still has HTTP overhead per update and holds connections open
  • Webhooks are event-driven and efficient, but require a publicly reachable server endpoint and idempotent handlers
  • WebSockets give you the lowest latency and true bidirectionality, but add operational complexity around stateful connections and horizontal scaling
  • SSE is a good fit when you only need server-to-client push and want to stay on plain HTTP
  • Match the pattern to your needs: polling for simplicity, webhooks for server-to-server, WebSockets for real-time interactive experiences

This completes System Design Intermediate. Next up: [Advanced] Design a URL Shortener — applying everything you've learned to a real system design interview question.

Enjoyed this breakdown?

Get new lessons in your inbox.