Webhooks

Webhook vs WebSocket vs Server-Sent Events

Three ways to push data to clients — they solve different problems. Here's how to pick the right one.

April 20259 min read

They All Answer the Same Question — Badly

At some point in every project, you hit the same wall: something changes on the server, and you need the client to know about it. The naive answer is polling — just ask the server every few seconds. It works, it's simple, and it's almost always the wrong long-term answer.

So developers reach for one of three alternatives: webhooks, WebSockets, or Server-Sent Events. The confusion is that all three feel like they solve the same problem. They don't. Each one is optimized for a completely different scenario, and picking the wrong one leads to infrastructure headaches, unnecessary complexity, or an integration that simply won't work in the real world.

This guide cuts through the surface-level comparisons and gives you a clear mental model for which tool belongs in which situation — along with the real-world gotchas that trip people up.

Webhooks: Built for Server-to-Server Communication

A webhook is an HTTP POST request that a remote service sends to your server when something happens. That's it. No persistent connection, no special protocol — just a plain HTTP request with a JSON body, fired at a URL you registered in advance.

The canonical example is Stripe webhooks: you register a URL in your Stripe dashboard, and whenever a payment succeeds (or fails, or a subscription renews), Stripe sends a POST to that URL with an event payload. Your server handles it, acknowledges with a 200 OK, and moves on.

Webhooks are the right tool when:

  • A third-party platform needs to notify your backend that something happened (payment, commit push, form submission, user signup).
  • The communication is server-to-server. Neither end is a browser.
  • Events are infrequent enough that a persistent connection would be wasteful.

The critical limitation: browsers can't receive arbitrary incoming connections. Your laptop doesn't have a publicly routable IP. So webhooks are a non-starter if your goal is to update a browser tab in real time. For that you need one of the other two.

Webhook payload shapes vary wildly between providers

Stripe, GitHub, and Shopify all send slightly different JSON structures. If you're handling events from multiple providers, it helps to inspect the raw payload before writing handler logic. JsonFormatter.ai is handy for pretty-printing and exploring the JSON structure of an incoming webhook payload without setting up a whole local environment first.

WebSockets: Full-Duplex, Persistent, Powerful — and Expensive

A WebSocket is a persistent, full-duplex connection between a client and a server. Both sides can send messages to each other at any time, with low latency, over a single TCP connection. It starts as a regular HTTP request that gets upgraded via the HTTP upgrade mechanism, switching from http:// to ws:// (or wss:// for encrypted). The WebSocket RFC 6455 defines the protocol.

Opening a WebSocket connection from a browser looks like this:

const socket = new WebSocket('wss://api.example.com/ws');

socket.addEventListener('open', () => {
  socket.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
});

socket.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
});

socket.addEventListener('close', () => {
  console.log('Connection closed — reconnect logic here');
});

That connection stays open. The server can push messages whenever it wants. The client can send messages back. This makes WebSockets the right choice for:

  • Chat apps — messages need to flow in both directions with minimal latency.
  • Live collaboration tools (like Figma or Google Docs) — multiple clients sending cursor positions, edits, and presence state simultaneously.
  • Multiplayer games — high-frequency, low-latency bidirectional state sync.
  • Trading dashboards where users can also submit orders or change watchlists.

The downside is infrastructure cost. Every open WebSocket is an open TCP connection. A server handling 10,000 concurrent users needs 10,000 persistent connections alive. That means you need long-running processes, sticky sessions (or a pub/sub layer like Redis), and careful connection management. Traditional request-per-thread server models don't scale here — you want Node.js, Go, or a framework built for async I/O.

WebSockets and HTTP/2 don't mix cleanly

Some load balancers and proxies have historically struggled with WebSocket upgrades over HTTP/2. If you're behind a CDN or reverse proxy, test your WebSocket upgrade path explicitly — don't assume it works because HTTP requests do.

Server-Sent Events: Underrated, Simple, and Often the Right Call

Server-Sent Events (SSE) are a one-way stream from server to browser over a long-lived HTTP connection. The browser opens the connection using the EventSource API — and then just listens. The server pushes events as plain text, formatted like this:

data: {"type":"price_update","ticker":"AAPL","price":182.44}

data: {"type":"price_update","ticker":"MSFT","price":415.70}

On the client side, consuming that stream is dead simple:

const source = new EventSource('/api/prices/stream');

source.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  updateUI(data);
});

source.addEventListener('error', () => {
  // EventSource automatically reconnects — no manual retry logic needed
});

There's a lot to like here. SSE works over plain HTTP — no protocol upgrade, no special infrastructure. It goes through proxies cleanly. It works over HTTP/2, which multiplexes streams over a single connection. And the browser handles reconnection automatically — if the connection drops, EventSource retries, picking up with the last event ID if the server supports it.

SSE is the right tool for scenarios where the browser needs a live feed but doesn't need to send data back. Good examples:

  • Live notification feeds — new alerts appear without a page refresh.
  • Build/job progress bars — stream log lines to the browser as a CI job runs.
  • Dashboards that update in real time — stock prices, sensor readings, live metrics.
  • AI response streaming — exactly how ChatGPT and Claude stream tokens to the browser.

The MDN guide to using Server-Sent Events is one of the clearest references out there — worth reading before you implement anything on the server side.

The Decision Framework

Stop asking "which is best?" and start asking "what problem am I actually solving?" Walk through these questions:

1

Is the recipient a server or a browser?

If a third-party service needs to notify your backend when something happens — use a webhook. Browsers can't receive incoming connections from the open internet.

2

Does the browser need to send data back over the same connection?

Yes → WebSocket. Chat messages, game actions, collaborative edits — anything where the client is also a producer, not just a consumer.

3

Is the browser just receiving a live feed?

No need to send data back → SSE. Simpler infrastructure, works over plain HTTP, automatic reconnection. Most people who reach for WebSockets for dashboards or notification feeds should be using SSE instead.

4

None of the above?

Low-frequency updates, simple use case, update interval doesn't need to be sub-second → just poll. A setInterval fetching from a REST endpoint every 10 seconds is the right answer more often than people admit.

Side-by-Side Comparison

FactorWebhookWebSocketSSE
ProtocolHTTP/S (plain POST)ws:// / wss://HTTP/S (long-lived)
DirectionOne-way (server → your server)Full-duplex (both ways)One-way (server → browser)
ConnectionStateless, per-eventPersistent, long-livedPersistent, long-lived
Typical use casePayment events, git hooks, SaaS integrationsChat, multiplayer, live collaborationLive feeds, notifications, streaming responses
Infrastructure complexityLow — any HTTP server worksHigh — needs async runtime, sticky sessions or pub/subMedium — needs long-lived response streams
Browser supportN/A (server-side only)UniversalUniversal (except IE)
Auto-reconnectN/ANo — manual implementationYes — built into EventSource
Works through HTTP proxiesYesSometimes (requires upgrade support)Yes

Classic Mistakes People Make

Knowing the right tool matters less if you've already been burned by using the wrong one. Here are the mistakes that come up again and again:

Using WebSockets when SSE would do

WebSockets have more developer cachet — they feel more "real-time." So teams reach for them even when they're building something like a live notification badge or a streaming progress indicator. The result: WebSocket server infrastructure, reconnection logic written by hand, and stateful connections that make horizontal scaling painful. SSE would have been three lines of code. If your client never sends anything back, WebSocket is almost certainly overkill.

Using webhooks to update a browser tab

Webhooks go server-to-server. You can't register localhost:3000 in the Stripe dashboard and expect a browser tab to receive it. What some developers try to do is poll their own backend to check if a webhook arrived — which is just polling with extra steps. The proper pattern is: webhook hits your server → server updates its state → SSE or WebSocket pushes the change down to the open browser tab.

Polling when SSE would be 10x cleaner

Polling every 5 seconds to show live job logs is something a surprising number of production systems still do. It burns unnecessary API calls, feels laggy, and creates a thundering herd if you have many users watching simultaneously. An SSE endpoint that streams lines as they're written is simpler to implement than people expect, especially in frameworks like Next.js that have good support for streaming responses.

Forgetting that WebSockets need reconnection logic

The EventSource API reconnects automatically. The WebSocket API does not. Network hiccups, server restarts, and idle timeouts will close the connection silently. If you don't have reconnection logic (with exponential backoff), your users will find themselves staring at a frozen UI wondering why messages stopped arriving.

Real-World Hybrid: How Production Apps Actually Work

The cleanest production architectures don't pick one — they use webhooks, databases, and SSE together in a pipeline that does exactly what each technology is good at.

Here's a concrete example. A SaaS product wants to show users a real-time notification when their Stripe payment succeeds:

1

Stripe sends a webhook to /api/webhooks/stripe

Plain HTTP POST. Your server receives the payment_intent.succeeded event, verifies the signature, and returns a 200.

2

Your server updates the database

Mark the order as paid. Write an event to a notifications table with the user ID and event data.

3

SSE pushes the update to the browser

The user's browser has an open EventSource connection to /api/notifications/stream. When the new notification row is inserted, the server pushes it down the SSE stream. The browser shows a toast notification instantly.

Each technology does one thing well. The webhook handles server-to-server event delivery from Stripe — no polling needed. The database acts as durable state. SSE handles the browser update — no WebSocket infrastructure needed because the browser isn't sending anything back.

You only need to swap SSE for WebSockets in step three if the user interaction model changes — say, if the user can respond to a notification or trigger an action from the same connection. Otherwise, keep it simple.

Keep Reading