# Webhook vs Web Socket: Picking an Event Channel

Published: November 8, 2026

Webhook vs web socket, explained for developers: how each delivers events, when to use which, and how to wire inbound email into an agent.

A webhook is an HTTP request the provider sends to your server when something happens. A web socket is a persistent, bidirectional TCP connection your client opens and keeps alive to stream messages in both directions. The short version of webhook vs web socket: use a webhook when a backend service needs to notify another backend about a discrete event, and use a WebSocket when a client needs a live, low-latency, two-way stream.

Almost every "which one should I use" argument collapses once you ask two questions. Who initiates the connection, and does the receiver need to talk back on the same channel? Webhooks are server to server, one direction, one event per request. WebSockets are client to server, continuous, and chatty in both directions.

## Webhook vs web socket at a glance

| | Webhook | Web socket |
|---|---|---|
| Transport | HTTP POST per event | Single upgraded TCP connection (`ws://` / `wss://`) |
| Who connects | Provider connects to you | Your client connects to the server |
| Direction | One way, provider to receiver | Bidirectional |
| Receiver must be | Publicly reachable HTTPS endpoint | Any network client, including browsers behind NAT |
| State | Stateless, each delivery independent | Stateful, connection has a lifetime |
| Delivery guarantee | Provider retries on failure, typically with backoff | Nothing after disconnect unless the app replays |
| Ordering | Not guaranteed | Ordered within a connection |
| Scaling cost | Scales with event volume | Scales with concurrent connections |
| Failure mode | Dropped or duplicated deliveries | Dead connections, stale state after reconnect |
| Good for | Email received, payment settled, build finished | Chat, cursors, live dashboards, token streaming |

If you want the deeper comparison written from a different angle, we have two earlier pieces covering the same ground: [webhooks vs websockets](/blog/webhooks-vs-websockets) and [web socket vs web hook](/blog/web-socket-vs-web-hook). This post is about choosing between them for a specific job, especially agent event delivery.

## How each one actually works

### Webhook

You register a URL with a provider. When the event fires, the provider makes an HTTPS POST to that URL with a JSON body and usually a signature header. Your handler returns 2xx fast. If it returns 5xx, times out, or the TLS handshake fails, the provider queues a retry.

That is the whole protocol. There is no connection to maintain, no heartbeat, no reconnect logic. The cost is that you need a public endpoint and you need to handle retries idempotently, because at-least-once delivery means you will eventually see the same event twice.

### Web socket

The client sends an HTTP request with `Upgrade: websocket`. The server responds 101 and the connection stops being HTTP. From there both sides send frames whenever they want, with ping and pong frames keeping intermediaries from killing an idle connection.

```js
const ws = new WebSocket("wss://example.com/stream");

ws.onmessage = (e) => render(JSON.parse(e.data));
ws.onclose = () => scheduleReconnect(); // you own this
```

Those four lines hide the real work. Production WebSocket code needs reconnect with jittered backoff, a resume cursor so you can fetch what you missed while disconnected, and a plan for what happens when a load balancer recycles 50,000 connections at once.

## When a webhook is the right answer

Choose a webhook when all of these are true:

1. The receiver is a server you control, with a stable HTTPS address.
2. Events are discrete and relatively low frequency. Seconds or minutes apart, not milliseconds.
3. Each event triggers work rather than updating a view.
4. You want the provider to handle retries so a deploy or a crash does not lose events.

Inbound email, payment confirmations, CI results, CRM record changes, and document processing callbacks all fit. The event matters more than the latency, and you would rather redeliver than drop.

## When a web socket is the right answer

Choose a WebSocket when:

1. A browser, mobile app, or CLI needs updates and cannot accept inbound connections.
2. Latency is user visible. Typing indicators, collaborative editing, live price ticks.
3. The client sends data on the same channel, frequently.
4. You are streaming many small messages where per-message HTTP overhead would dominate, such as LLM tokens.

If the stream is one directional and you only need server to client push to a browser, Server-Sent Events is often simpler than a WebSocket and survives proxies better. Reach for a full WebSocket when the client talks back.

## The hybrid pattern most agent systems end up with

Real systems use both, at different layers.

The provider delivers an event to your backend over a webhook. Your backend does the durable work: verify the signature, write the event, enqueue a job. Then your backend pushes a lightweight notification to connected UI clients over a WebSocket so a human watching a dashboard sees the change immediately.

```
Provider  --webhook-->  your API  --queue-->  worker
                            |
                            +--websocket-->  browser dashboard
```

This keeps the durability guarantees where they belong, in a queue behind an HTTP endpoint, and keeps the fragile connection layer carrying nothing you cannot rebuild from your own database.

## Inbound email is a webhook-shaped problem

Email events are the textbook webhook case. A message arrives at an unpredictable time, it must not be lost, the handler often does real work like calling a model or hitting a vendor API, and the recipient is a backend service. There is no human staring at a screen waiting 40 milliseconds for it.

A WebSocket for inbound mail would be a bad trade. You would hold a connection open for hours to receive three messages, lose anything that arrived during a deploy, and rebuild retry logic that HTTP already gave you for free.

So that is how we built it. When mail arrives at a Robotomail mailbox, we POST to your registered webhook with `{ event, timestamp, data }`:

```json
{
  "event": "message.received",
  "data": {
    "message_id": "...", "mailbox_id": "...", "mailbox_address": "shopping-agent@robotomail.co",
    "from": "vendor@example.com", "subject": "Re: Order #A-4521 delivery update?", "body_text": "...",
    "thread_id": "...", "received_at": "2026-04-17T10:00:00.000Z"
  }
}
```

Create a mailbox first:

```bash
npx @robotomail/cli mailbox create shopping-agent
```

Or over the API:

```bash
curl -X POST https://api.robotomail.com/v1/mailboxes \
  -H "Authorization: Bearer rm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"address": "shopping-agent"}'
```

Then register a webhook and handle the delivery. Acknowledge immediately, process out of band:

```ts
import express from "express";
const app = express();
app.use(express.json());

app.post("/hooks/robotomail", async (req, res) => {
  const { event, data } = req.body;
  res.sendStatus(200); // ack first, work later

  if (event !== "message.received") return;
  if (await alreadySeen(data.message_id)) return; // idempotency

  const reply = await agent.run(data.body_text);

  await fetch(
    `https://api.robotomail.com/v1/mailboxes/${data.mailbox_id}/messages`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ROBOTOMAIL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: [data.from],
        subject: `Re: ${data.subject}`,
        bodyText: reply,
        inReplyTo: data.message_id,
      }),
    },
  );
});

app.listen(3000);
```

Two details matter more than they look. `alreadySeen(message_id)` is what makes at-least-once delivery safe. `inReplyTo` is what keeps the reply in the same thread instead of starting a new one, which is covered in [email threading](/docs/concepts/threading).

For the full payload reference and signature verification, see [webhook concepts](/docs/concepts/webhooks). For parsing details like HTML bodies, quoted replies, and attachments, see [parsing an inbound email webhook](/blog/parse-inbound-email-webhook).

## Operational differences that bite in production

**Retries.** Webhook providers retry; WebSocket servers do not resend frames you missed while offline. If you pick a WebSocket for anything durable, you must build a cursor or sequence number and a replay endpoint.

**Ordering.** Webhook deliveries can arrive out of order, especially after a retry. Do not infer sequence from arrival order. Use timestamps or thread state from the payload.

**Timeouts.** Most webhook senders cut you off after a short window, often a handful of seconds. Running a model inside the handler is how you turn one event into five duplicate deliveries. Return 200, enqueue, process.

**Local development.** Webhooks need a public URL, so you tunnel during development or poll an API instead. WebSockets connect outward and work fine on localhost. This is the one place where WebSockets are genuinely easier.

**Scaling.** Webhook load is request shaped and fits normal autoscaling. WebSocket load is connection shaped, which means memory per connection, sticky routing, and careful rolling deploys so you do not stampede reconnects.

**Security.** Webhook endpoints are public, so signature verification and timestamp checks are mandatory. WebSockets authenticate once at upgrade, which means you need a plan for tokens that expire mid-connection. For email specifically, treat inbound content as untrusted input; see our notes on [agent email security](/docs/guides/agent-email-security).

## FAQ

### Is a webhook faster than a web socket?

For a single event, no. A webhook pays TCP and TLS setup on every delivery, while a WebSocket already has an open connection. The gap is tens of milliseconds, which is irrelevant for email, payments, or CI callbacks and very relevant for live cursors and token streaming.

### Can I receive email over a web socket instead of a webhook?

You can build it, but you should not want to. Email arrival is sparse and must survive restarts, which is exactly what HTTP retries give you. If your runtime cannot accept inbound HTTP, poll the messages API on an interval instead of holding a socket open.

### What if my webhook endpoint goes down?

A sender with retries will back off and try again, so a short outage means delayed events rather than lost ones. Build the handler to be idempotent by keying on `message_id`, because recovery will replay things you may have already processed.

### Do I need both in the same application?

Often yes, at different layers. Webhooks carry events into your backend durably; a WebSocket pushes the resulting state to any UI a human is watching. Agents themselves only need the webhook. See [multi-agent architecture](/blog/multi-agent-architecture) for how that fans out across several agents.

Want inbound email delivered as clean JSON to an endpoint you already run, with threading and replies handled for you? Provision a mailbox and wire up a handler in a few minutes at [robotomail.com](https://robotomail.com), or start with the [quickstart](/docs/quickstart).
