Agents in production

Platform integration

Queue-first webhook architecture: why the receiver should never do the work

Queue-first webhook architecture: why the receiver should never do the work, how backpressure actually behaves, and where the pattern breaks in production.

9 minute read
Decorative imagery showcasing Pontil's brand

Every webhook receiver eventually meets the same problem. The sender fires an event. The receiver picks it up, does the work inline, and returns a 200. That works until traffic spikes, a downstream system slows down, or the receiver restarts mid-request. Then events vanish, retries hammer the wrong endpoint, and the on-call channel fills up.

Queue-first webhook architecture answers this by splitting the receiver into two jobs. The HTTP endpoint accepts the event, persists it, and acknowledges. A separate worker does the actual processing. It sounds obvious. Most teams still don't do it, because the inline pattern feels simpler and the failure modes only show up under load.

This piece takes a position: for anything past a prototype, the receiver should never do the work. Below, five sections on why the queue belongs at the front, how backpressure actually behaves, what changes for agents, where the pattern breaks, and what a production-grade setup looks like.

Why inline processing quietly fails

The inline pattern collapses two responsibilities into one HTTP handler: acknowledging the event and acting on it. Under normal load, this is fine. Under any of the four conditions webhook systems actually hit — traffic bursts, downstream slowness, receiver deploys, or partial failures — it fails in ways that look like other problems.

Senders typically retry on non-2xx responses or timeouts. If your handler takes 8 seconds because a downstream API is slow, and the sender's timeout is 5, the sender retries. You now process the same event twice. If you added idempotency at the top of the handler, you'll reject the retry — but only after the sender has already burned attempts and possibly moved the delivery to a dead letter. If you didn't, you've duplicated the side effect.

Worse: the receiver often has no memory of what it saw. If it crashes between accepting the request and finishing the work, the event is gone from your side. The sender thinks it delivered. You have no record. This is the class of failure a dead letter queue is designed to catch, but a DLQ only helps if something wrote the event down before the crash.

The fix isn't faster handlers. It's separating the two responsibilities.

What queue-first actually means

A queue-first receiver has three parts. An HTTP endpoint that does the minimum needed to accept the event. A durable queue that holds it. A worker that processes it asynchronously.

The HTTP endpoint's job is narrow:

  1. Verify the signature. See our guide on webhook signature verification with HMAC-SHA256 for the pattern.
  2. Persist the raw payload, headers, and a delivery ID to a durable store.
  3. Return 2xx.

That's it. No database writes to business tables. No calls to downstream APIs. No enrichment. If the handler can't finish those three steps in under 100ms, something is wrong at the infrastructure level, not the application level.

The queue itself can be a hosted broker (SQS, Pub/Sub, Kafka, RabbitMQ), a database-backed queue (Postgres with SELECT ... FOR UPDATE SKIP LOCKED, or something like river or Oban), or a durable execution platform like Temporal that exposes queue-like semantics through task queues backed by workflow state. The choice matters less than the property: once the event is durably recorded, it survives receiver restarts, worker crashes, and downstream outages.

The worker pulls from the queue, does the actual work — updating your database, calling downstream services, notifying agents — and acknowledges the message only after the work completes. If it crashes mid-processing, the message becomes visible again after the visibility timeout and another worker picks it up.

Inline receiver
Queue-first receiver

Acknowledgement latency

Depends on downstream work

Bounded by signature verify + write

Behaviour under downstream outage

Timeouts, sender retries, duplicate work

Queue depth grows, work pauses cleanly

Behaviour on receiver crash mid-request

Event lost

Event preserved in queue or re-delivered by sender

Retry semantics

Sender retries entire path

Worker retries independently with own [backoff](https://www.pontil.com/blog/webhook-retry-with-exponential-backoff-a-practical)

Backpressure control

None — sender decides

Queue depth is the control point

Ordering guarantees

Implicit from request order

Explicit — per-key FIFO or unordered

Backpressure is the point, not a side effect

Most writing on webhook queues frames the queue as a durability mechanism. It is, but that undersells it. The bigger property is backpressure — the ability to let the queue absorb load the workers can't yet process, without breaking the contract with the sender.

Without a queue, backpressure has nowhere to live. If the receiver is overloaded, it either slows down (and the sender times out) or crashes (and the sender retries). Both are lossy in practice. The sender's retry policy — usually exponential backoff with a cap on attempts — is designed to survive brief hiccups, not sustained downstream slowness. If your worker pool needs 20 minutes to catch up because a downstream partner is degraded, sender retries will move events to their dead letter long before you're ready to accept them.

With a queue in front, the shape changes. The HTTP endpoint keeps returning 200 because it's only writing to the queue. Queue depth grows. Workers process at the rate they can sustain. When the downstream recovers, workers catch up. The sender sees a healthy endpoint the whole time.

This only works if you actually monitor queue depth and worker throughput. A queue that grows without alerts is a delayed outage. Two metrics matter more than any others:

  • Queue depth over time. Rising steadily during a bad period is fine. Rising without recovering is not.
  • Oldest message age. The time between a message entering the queue and being processed. When this crosses your SLA — say, 5 minutes for user-visible workflows — you need to know before customers do.

Alerting on both catches the two failure modes: workers falling behind steadily (depth) and workers stuck on a poison message that's blocking the head (age).

What changes when agents are the consumer

Webhooks in a purely system-to-system context have well-understood semantics. When agents are downstream of the webhook — reacting to a Stripe charge, a Salesforce record change, a support ticket update — three things get harder.

Fan-out is asymmetric. One incoming webhook can trigger multiple agent actions, some of which want the event immediately and some of which want it batched or deduplicated. A queue-first architecture lets you write once and read many: the raw event lands in the queue, and different consumers pull it for different purposes. Inline processing forces you to decide, at request time, what all the downstream reactions are. You never actually know that.

Latency budgets are visible. An agent waiting for a webhook to trigger the next step is measuring end-to-end time — from the source system's event to the agent's next tool call. If your receiver takes 3 seconds because it's doing the work inline, that's 3 seconds the agent's context is stalled. A queue-first receiver returns in under 100ms and the worker handles processing on its own clock. The agent gets the resulting state change when the worker finishes, but the source system isn't waiting on you.

Idempotency becomes a runtime property, not a database constraint. When agents are consuming the events downstream of your queue, you can no longer assume that "process this event once" means "call the database once." It means "reach a consistent final state at the tool boundary regardless of how many times the worker retries." That's a design constraint on the tools, not just on the handler.

This is why the pattern matters beyond hyperscaler webhook volumes. Any team building agents on top of event-driven signals ends up needing the same shape.

Where the pattern breaks

Queue-first isn't free, and honest deep-dives should name the trade-offs.

Ordering guarantees weaken. A single-threaded inline handler processes events in the order they arrive. A worker pool with N workers pulling from a queue does not, unless you partition by key. If ordering matters — say, all events for a given account must process in order — you need a partitioned queue or a key-based routing layer. Most brokers support this (Kafka partitions, SQS FIFO with message group ID, Pub/Sub ordering keys), but you have to design for it. Losing implicit ordering silently is a common bug.

Debugging gains a hop. With inline processing, a failed webhook is one log line. With queue-first, you have three: the HTTP request, the queue enqueue, the worker attempt. Tracing across those needs a delivery ID propagated through all of them and a log aggregator that can join on it. Without that, debugging becomes archaeology.

Poison messages block progress. If a specific event causes the worker to crash — bad payload, unhandled edge case, downstream returning a permanent error — that event will be re-delivered until it hits the max attempts and moves to the DLQ. If you don't configure max attempts, or set them too high, a single bad message can stall the queue behind it (for FIFO queues) or waste worker capacity (for unordered ones). This is well-covered ground; the fix is disciplined DLQ handling and alerting on DLQ depth, not avoiding queues.

The queue itself becomes infrastructure you own. Broker outages, disk-full events on the queue host, mis-set visibility timeouts — these are now your problems. Managed queues push most of this to the cloud vendor, but not all. Self-hosted brokers push it all back to you.

None of these are reasons to skip the pattern. They're reasons to implement it deliberately.

How Pontil fits

Webhooks are one half of the event story for agent projects. The other half is the outbound tool call — how the agent acts on the state change that just arrived. Both halves share the same production discipline: durable delivery, retries with backoff, idempotency at the boundary, per-user identity on every action.

Pontil sits in the tools layer of the agent stack. We generate the tools agents call and run them through a managed runtime — the same runtime that handles retries, failure classification, and executing as the authenticated user rather than a shared service account. When a queue-first webhook receiver hands work to a worker that then invokes agent tools, the runtime is the boundary that decides whether the retry is safe, whether the user still has permission, and whether the downstream call went through. See our product overview for how the pieces connect.

What's next for the receiver you haven't built yet

Most teams reach queue-first by accident, after an incident. The receiver went down during a spike, or a downstream slowness burned through sender retries, or a poison message loop chewed through capacity. The rebuild that follows is usually the queue-first design described above.

The better path is to start there. Signature verification, persist, ack, return — everything else is a worker's problem. Once that split is in place, you can add backpressure monitoring, DLQ handling, key-based ordering, and consumer fan-out without rewriting the receiver.

The harder question is what runs behind the queue. Workers that update your own database are straightforward. Workers that call downstream APIs — or agent tools — need the same discipline the receiver just gained: bounded retries, idempotent operations, per-user identity, and observability across every hop. That's where the next round of production work usually lands.

Join our weekly newsletter

Stay up to date on the ever changing agentic landscape.

POSTS

Related content

Platform integration

Agent infrastructure

Webhook dead letter queue: a practical guide to handling failed webhooks

7 minute read

Platform integration

Agent infrastructure

Webhook retry with exponential backoff: a practical implementation guide

7 minute read

Agent infrastructure

Platform integration

Webhook reliability patterns: how to deliver events agents can actually trust

9 minute read