# Webhook vs WebSocket: One HTTP Request or One Open Connection

> A webhook is one HTTP POST sent to your server per event. A WebSocket is a two-way connection the client keeps open. Wire format, guarantees, when to use each.

Source: https://webhooker.eu/blog/webhook-vs-websocket
Last updated: 2026-09-21

A webhook is a single HTTP POST request that a provider’s server sends to a public URL on your server when an event happens, and the connection closes as soon as you answer. A WebSocket is one long-lived, two-way connection that a client opens to a server and keeps open, so either side can send messages at any time. Use webhooks for occasional events between servers that belong to different companies, and WebSockets for frequent, low-latency messages to a browser or app that stays connected.

Both get called “real time”, which is why the WebSocket vs webhook question keeps coming up as if you had to pick one. They solve different halves of the problem, and most products that show a live update after a payment use both. This post compares them at the HTTP level: what each sends, who has to be reachable, what is lost when something fails, and what each costs to run. For the basics, see [what a webhook is](https://webhooker.eu/blog/what-is-a-webhook) and [how one webhook exchange works](https://webhooker.eu/blog/how-do-webhooks-work). The pull side of the story is in [webhooks vs APIs](https://webhooker.eu/blog/webhook-vs-api).

## What is the difference between a webhook and a WebSocket?

With a webhook, the sender connects to you, delivers one event and hangs up. With a WebSocket, the receiver connects to the sender once and both sides talk over that connection until it drops.

*Table: webhooks and WebSockets compared on direction, connection lifetime, reachability, delivery guarantees, authentication and scaling.*

|  | Webhook | WebSocket |
| --- | --- | --- |
| Who initiates | The sender, once per event | The client, once per session |
| Connection lifetime | One request and response | Minutes to days |
| Direction | One way, sender to receiver | Both ways |
| Who needs a public address | The receiver | The server only; the client dials out |
| Protocol | Plain HTTPS POST | HTTP Upgrade, then WebSocket frames ([RFC 6455](https://www.rfc-editor.org/rfc/rfc6455)) |
| Delay | Usually seconds | Roughly one network trip |
| If the receiver is down | Provider retries, at-least-once | Message is gone unless you built resume |
| Authentication | HMAC signature on every request | Credentials once, at the handshake |
| Server state | None between requests | One open socket per client |
| Typical receiver | A backend service | A browser, mobile app or bot process |

## What does each one look like on the wire?

A webhook is ordinary HTTP. Here is a Stripe-style delivery, shortened:

```http
POST /webhooks/stripe HTTP/1.1
Host: api.yourapp.com
Content-Type: application/json
Stripe-Signature: t=1789984800,v1=5257a869e7ec...08d8bd

{"id":"evt_1Qx...","type":"payment_intent.succeeded","data":{"object":{"id":"pi_3Qx...","amount":4900,"currency":"eur"}}}
```

Your server answers `HTTP/1.1 200 OK` with an empty body and the exchange is over. The next event is a new request, possibly hours later. Nothing connects the two except the secret you use to [verify the signature](https://webhooker.eu/blog/webhook-security-signature-verification).

A WebSocket also starts as HTTP, but only for one round trip. The client sends a GET that asks to switch protocols. This is the example from RFC 6455:

```http
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
```

The server agrees:

```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```

`Sec-WebSocket-Accept` is a SHA-1 hash of the client’s key joined with a fixed GUID, base64-encoded. It proves the server understood the upgrade, and that is all it proves: it is not authentication. After the `101`, HTTP is finished, and the same TCP connection carries small binary frames in both directions, with no headers, no status codes and no request-response pairing. Ping and pong frames let each side check the other is still there.

A webhook pays the full cost of an HTTP request for every event and gets HTTP’s status codes and retry semantics in return. A WebSocket pays that cost once, then sends messages for a few bytes of framing each, and you get no acknowledgement unless you design one.

## Who has to be reachable from the internet?

With a webhook, you do. Stripe says registered endpoints [“must be publicly accessible HTTPS URLs”](https://docs.stripe.com/webhooks). A provider cannot reach a laptop, a server behind NAT, or a service inside a private network, which is why local development needs a [tunnel or a forwarding CLI](https://webhooker.eu/blog/ngrok-alternatives-for-webhooks).

With a WebSocket, the client dials out. Outbound connections pass through NAT and most firewalls without any configuration, and RFC 6455 was designed to work [“over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries”](https://www.rfc-editor.org/rfc/rfc6455#section-1.1). That is why some platforms offer a WebSocket mode to developers who cannot expose a port.

The catch is that intermediaries do not like idle connections. NGINX, for example, [closes a proxied WebSocket](https://nginx.org/en/docs/http/websocket.html) if the backend sends nothing for 60 seconds unless you raise `proxy_read_timeout` or send ping frames. Every proxy, load balancer and mobile network on the path has its own timeout, so the application has to send heartbeats and reconnect when the connection drops anyway.

## What happens to a message when something fails?

A webhook has an acknowledgement built in: your status code. Anything other than a 2xx tells the provider the delivery failed, and most providers then retry on a schedule. Stripe retries [“for up to three days with an exponential back off”](https://docs.stripe.com/webhooks#automatic-retries) in live mode. Each event is an independent request, so it can be stored, retried and replayed on its own. The price is [at-least-once delivery](https://webhooker.eu/blog/at-least-once-vs-exactly-once-webhooks): duplicates arrive, order is not guaranteed, and your handler needs [idempotency keys](https://webhooker.eu/blog/webhook-idempotency-keys).

A WebSocket has none of that. TCP guarantees order and delivery only while the connection lives. If the client is offline for thirty seconds, every message sent during those thirty seconds is lost, and the protocol has no retry or replay. If you need it, you build it: sequence numbers, a server-side buffer, and a resume step on reconnect.

The platforms that use WebSockets for events did exactly that. Discord’s Gateway has a Resume operation: the client reconnects with its session id and the last sequence number it saw, and the Gateway will [“send the missed events in order”](https://docs.discord.com/developers/events/gateway). Slack’s Socket Mode requires the app to acknowledge every envelope by its `envelope_id` [“so that Slack knows whether to retry”](https://docs.slack.dev/apis/events-api/using-socket-mode/). Both are application protocols on top of the socket. A bare WebSocket gives you neither, and a dropped one shows up only as a gap in the stream, if you numbered the messages.

## How is each one authenticated?

A webhook is authenticated per message. The provider computes an HMAC of the request body with a secret you share and puts it in a header, such as `Stripe-Signature` or GitHub’s `X-Hub-Signature-256`. You recompute it over the raw bytes and compare. No request can lean on an earlier one, so each is checked. A timestamp inside the signed data [stops old requests being replayed](https://webhooker.eu/blog/webhook-replay-attacks-timestamp-tolerance). Stripe’s libraries default to a five-minute tolerance.

A WebSocket is authenticated per connection, at the handshake. RFC 6455 [does not prescribe a method](https://www.rfc-editor.org/rfc/rfc6455#section-10.5): the server can use anything an HTTP server can, such as cookies, HTTP authentication or TLS client certificates. In a browser the [`WebSocket` constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) takes only a URL and a list of subprotocols, so tokens usually travel in a cookie, in the query string, or in the first message after connecting. Discord does the last: the first thing a bot sends is an Identify payload carrying its token.

Two details follow. A browser’s same-origin rules do not block a page on another site from opening a WebSocket to your server, and cookies can travel with the handshake, so the server has to [check the `Origin` header](https://www.rfc-editor.org/rfc/rfc6455#section-10.2) itself. And once a connection is accepted, it stays trusted until it closes. If a token expires or a user is logged out, the server has to close their sockets on purpose.

## What does each one cost to run?

A webhook receiver is a stateless HTTP handler. Any instance behind the load balancer can take any request. Scaling means adding instances, and a deploy drops nothing because a provider whose request fails mid-restart will retry. Traffic is spiky, though. Stripe recommends [an asynchronous queue](https://docs.stripe.com/webhooks#handle-events-asynchronously) behind the handler for bursts such as the first of the month, when subscriptions renew together.

A WebSocket server holds state for every connected client, whether or not any messages flow. Routing, deploys and capacity planning all change as a result.

Routing first. A message for user 4812 has to reach the one process holding that user’s socket. With more than one server you need a shared bus between them. Socket.IO’s documentation covers both halves: [an adapter to pass messages between nodes](https://socket.io/docs/v4/using-multiple-nodes/), and sticky sessions at the load balancer whenever its HTTP long-polling fallback is enabled.

Deploys get louder. Restarting a server closes every connection on it, and all those clients reconnect at the same moment. Reconnect logic needs backoff with jitter for the same reason [webhook retries do](https://webhooker.eu/blog/webhook-retries-exponential-backoff).

And you plan capacity in concurrent connections instead of requests per second. Ten thousand idle users still hold ten thousand sockets, file descriptors and heartbeat timers.

## Where do Server-Sent Events and polling fit?

Server-Sent Events are the one-way version of a WebSocket. The browser opens a normal HTTP request, the server answers with `Content-Type: text/event-stream` and keeps writing events into the response. The client cannot send anything back on that connection. In return it stays plain HTTP, so proxies and your existing authentication handle it unchanged. Reconnection is built in: the browser [reconnects on its own](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) and sends the last event id it received in a `Last-Event-ID` header, so the server can resume the stream. One limit to know: over HTTP/1.1 browsers allow six connections per domain across all tabs, so serve SSE over HTTP/2.

If the browser only needs to receive, SSE is usually less work than a WebSocket. Choose a WebSocket when the client sends often too: chat, multiplayer state, collaborative cursors.

Polling is the client asking on a timer. It needs no public endpoint and no open connection, and the delay equals the interval. It fits small, non-urgent syncs, and it stays useful as a reconciliation job that catches webhooks that never arrived. [Webhooks vs APIs](https://webhooker.eu/blog/webhook-vs-api) covers that trade in full.

## When should you use a webhook, and when a WebSocket?

Use a webhook when:

- the receiver is a server, and it can expose an HTTPS URL
- the sender is another company, or a system you do not control
- events are occasional: a payment cleared, a build finished, an order shipped
- losing an event costs more than a delay of a few seconds
- you want a record of every delivery attempt

Use a WebSocket when:

- the receiver is a browser, a mobile app or a process that cannot accept inbound connections
- both sides send, and messages are frequent
- a delay of seconds would be noticed: chat, live prices, presence, games
- a missed message is tolerable, or you are ready to build acknowledgement and resume

If the receiver is a browser that only listens, use SSE. If nothing is urgent, poll.

Real platforms split along these lines. [Stripe](https://docs.stripe.com/webhooks) and [GitHub](https://docs.github.com/en/webhooks/about-webhooks) deliver events as webhooks, because their receivers are backend services with a URL. Slack delivers the Events API to an HTTP Request URL and also offers [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) so an app can receive the same events “without exposing a public HTTP Request URL”. That suits apps that cannot accept inbound HTTP requests, with limits: at most 10 open connections per app, a connection refresh every few hours, and no listing in the public Slack Marketplace. Discord goes the other way. Its WebSocket Gateway is the main transport, [most events are only available there](https://docs.discord.com/developers/events/overview), and webhook events over HTTP cover a small number of event types.

## Can you use webhooks and WebSockets together?

Yes, and for anything a user watches happen, you usually should. Each leg of the path gets the transport that suits it:

```text
Stripe --webhook--> your backend --WebSocket or SSE--> browser
```

Take a checkout page. The customer pays, and the page shows a spinner. Stripe confirms the payment and sends `payment_intent.succeeded` to your webhook endpoint. Your backend verifies the signature, stores the event, marks the order paid and returns 200. Then it publishes “order 1042 paid” to the socket or event stream that the customer’s browser holds open, and the spinner becomes a receipt.

The webhook leg is the durable one. If your backend is down for an hour, Stripe retries and the order still gets marked paid. The browser leg is best effort. If the tab lost its connection, the page fetches the order status from your API when it reconnects. Keep it that way round: the database is updated by the webhook, and the socket only tells the page to look.

## Where a webhook gateway fits

We build Webhooker, so treat this paragraph as a vendor’s note. A [webhook gateway](https://webhooker.eu/blog/what-is-a-webhook-gateway) handles the server-to-server leg in the diagram above, and only that leg. Webhooker is hosted in the EU. The provider posts to an ingest URL such as `https://app.webhooker.eu/in/{token}`, the signature is verified there, and the event is stored and forwarded to your backend with six attempts over about five hours, then held in a [dead-letter queue you can replay](https://webhooker.eu/blog/webhook-dead-letter-queue-replay). It does not push to browsers and does not replace a WebSocket or SSE server. If you want to see what a provider sends before writing a handler, the free [webhook-tester](https://webhooker.eu/tools/webhook-tester) shows every request, and you can [create an ingest URL](https://app.webhooker.eu/register) when you want the retries handled for you.

## Frequently asked questions

### Is a webhook the same as a WebSocket?

No. A webhook is one HTTP POST from a provider’s server to a public URL on your server, sent when an event happens and finished when you return a status code. A WebSocket is a persistent two-way connection that a client opens with an HTTP Upgrade request and keeps open.

### Are webhooks push or pull?

Push. The provider sends the HTTP request to your URL when the event happens, and your server never asks for it. Polling is the pull version of the same job: your code calls the provider’s API on a timer and asks what changed. A WebSocket is push as well, but over a connection the client opened first.

### Are WebSockets faster than webhooks?

Per message, yes. A WebSocket has already paid for the TCP and TLS handshakes, so a message costs a small frame and one network trip. A webhook is a full HTTP request, and most providers send it from a queue, so it usually arrives within seconds of the event. Nobody notices that gap on a payment confirmation. In chat or on a price ticker they do.

### Can a webhook send data to a browser?

Not directly. A webhook needs a public URL to POST to, and a browser does not have one. The usual pattern is to receive the webhook on your backend, then push the update to the browser over a WebSocket or Server-Sent Events connection that the page opened earlier.

### Should I use a WebSocket instead of webhooks between two servers?

Rarely. It helps when the receiving server cannot accept inbound connections, which is why Slack offers Socket Mode and Discord bots use the Gateway. Otherwise you take on reconnect handling, heartbeats, and your own acknowledgement and replay, while most webhook providers retry failed deliveries for you.

### What is the difference between webhooks, WebSockets and SSE?

A webhook is a server-to-server HTTP POST per event. A WebSocket is a long-lived two-way connection, normally between a browser or app and a server. Server-Sent Events are a long-lived one-way HTTP response, server to browser, with automatic reconnection and resume through `Last-Event-ID`. With polling, the fourth option, the client asks on a timer.
