Comparisons·9 min read·2026-02-06·Colm Byrne, Technical Product Manager

How Hookdeck's At-Least-Once Guarantee Changes How You Build Integrations

An at-least-once delivery guarantee doesn't mean perfect delivery. It means duplicates are real and idempotency isn't optional. Hookdeck teaches you that the hard way — and it's a better developer.

"At-least-once delivery" sounds like a promise. It is — but there is a clause buried in that promise that most developers do not read until they get paged at 2am: the same event can arrive twice. Or three times. Or out of order.

Hookdeck does not hide this. The Hookdeck documentation is honest about what at-least-once means architecturally, and that honesty forces you to build correctly. An event gateway that tells you the truth about its delivery semantics is a gift, even when the truth is uncomfortable. See also the webhook vendor evaluation checklist for how to compare delivery semantics across tools.

Most webhook processors just say "we retry on failure" and leave you to figure out the implications. Hookdeck says "we guarantee at-least-once, here is what that means for idempotency, here is the event ID you should use for deduplication." That explicitness is the real product.


What At-Least-Once Actually Means in Practice

Hookdeck accepts your event at ingress, stores it durably, and retries delivery up to 50 times using exponential backoff. The guarantee: the event will reach your destination eventually, as long as your destination eventually returns a 2xx. The necessary consequence: your destination might receive the same event more than once.

Every retry is a potential duplicate. Hookdeck sends an event ID in the delivery headers — specifically X-Hookdeck-Event-Id. That ID is stable across retries. The same event, retried five times, carries the same ID all five times. This is the deduplication handle. Hookdeck gives you the tool; using it correctly is your job.

This is not a criticism of Hookdeck. It is the only honest design for a system that provides delivery guarantees. You cannot have at-least-once delivery without duplicates being theoretically possible. The alternative — exactly-once delivery — requires coordination protocols (two-phase commit, or distributed locks) that introduce latency and failure modes of their own. Hookdeck made the right tradeoff. At-least-once with explicit deduplication handles is the correct architecture for high-volume event delivery.

But the implication is real: idempotency is not optional when using Hookdeck. It is the price of admission. If your handler does not deduplicate on the Hookdeck event ID, you are building on a foundation you do not understand.


Svix Points in the Opposite Direction

Svix is solving a fundamentally different problem: outbound webhook delivery, not inbound receipt. Understanding the difference clarifies what Hookdeck is actually for.

Svix is an outbound webhook platform — you use it when you are the provider sending webhooks to your customers, not the consumer receiving them from a provider. Svix handles the retry logic, the delivery tracking, the customer-facing portal where your customers can see their webhook history and manage their endpoints. If you are building a SaaS product that needs to notify customer systems when something happens, Svix is what you reach for.

The delivery model is similar: exponential backoff, up to multiple retry attempts, durable storage of outbound events, 90-day event retention so your customers can inspect history through the portal. Svix also implies at-least-once delivery semantics. The idempotency obligation is the same — it lands on your customer's handler rather than yours.

The comparison is this: Hookdeck makes you better at receiving webhooks from providers like Stripe, GitHub, or Twilio. Svix makes you better at sending webhooks to your own customers. Both have retry infrastructure. Both push you toward idempotency. They serve different roles in the webhook supply chain — and neither can substitute for the other.

A team building a payment processing integration uses Hookdeck to receive Stripe events reliably. The same team, if they also need to notify their own customers about payment status, uses Svix to deliver those notifications. Both are excellent at their respective jobs.


Building the Idempotency Store

Whether you are using Hookdeck, Svix, or handling raw provider webhooks, the implementation of idempotency is the same problem. Here is the version that actually works under concurrent load.

The naive approach — check if the event ID exists, if not process it — has a race condition. Two simultaneous deliveries of the same event both pass the check before either writes the record:

T=0ms   Request A reads: no record for evt_abc
T=1ms   Request B reads: no record for evt_abc
T=5ms   Request A processes, inserts record
T=6ms   Request B processes, inserts DUPLICATE record

The fix is a database-level atomic claim using INSERT ... ON CONFLICT DO NOTHING ... RETURNING id. The INSERT and the processing happen inside the same transaction. Only one request can win the INSERT:

async function processWebhookEvent(eventId, eventType, payload) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');

    // Atomic claim — database serializes concurrent attempts
    const claim = await client.query(`
      INSERT INTO processed_webhook_events (event_id, event_type, status)
      VALUES ($1, $2, 'processing')
      ON CONFLICT (event_id) DO NOTHING
      RETURNING id
    `, [eventId, eventType]);

    if (claim.rows.length === 0) {
      // Another request already claimed this event — correct behavior
      await client.query('ROLLBACK');
      return { status: 'duplicate_skipped', eventId };
    }

    // Process inside the same transaction
    await processBusinessLogic(client, eventType, payload);

    await client.query(
      `UPDATE processed_webhook_events SET status = 'completed', processed_at = NOW()
       WHERE event_id = $1`,
      [eventId]
    );

    await client.query('COMMIT');
    return { status: 'processed', eventId };

  } catch (err) {
    await client.query('ROLLBACK');
    // Idempotency record rolled back — event stays retryable
    throw err;
  } finally {
    client.release();
  }
}

The event ID you pass as eventId comes from the Hookdeck delivery header, or from Stripe's event.id, or from Svix's delivery metadata. Regardless of source, the deduplication store looks the same.

The schema for the idempotency table matters:

CREATE TABLE processed_webhook_events (
  id SERIAL PRIMARY KEY,
  event_id VARCHAR(255) NOT NULL UNIQUE,
  event_type VARCHAR(100) NOT NULL,
  status VARCHAR(50) NOT NULL DEFAULT 'processing',
  processing_result JSONB,
  first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  processed_at TIMESTAMPTZ,
  attempt_count INTEGER NOT NULL DEFAULT 1
);

CREATE UNIQUE INDEX idx_processed_webhook_events_event_id
  ON processed_webhook_events (event_id);

Store the processing_result — the IDs of records created, the business outcome. This is the artifact you inspect during incidents. When something went wrong and you need to know whether that event produced a result, the idempotency store tells you.


Where HookTunnel Fills the Gap

HookTunnel is where this story connects. You are building the idempotency store. You are using Hookdeck event IDs as the deduplication key. But when an incident happens — an event that arrived, was claimed, but produced unexpected output — you need the original payload. See our guide on silent webhook failure for how missing payloads surface in production. And if you also receive Stripe duplicate webhook events, the same idempotency store handles both.

The idempotency store tells you whether the event was processed. HookTunnel tells you what was in it.

This distinction matters when you are debugging. Your processed_webhook_events table shows status = 'completed' for event ID evt_hookdeck_abc123. Something downstream is wrong. Did the payload contain unexpected data? Was the customer.id field missing? Was there a field name that changed in the provider's API? You cannot answer these questions from the idempotency record alone. You need the captured payload.

HookTunnel keeps every inbound HTTP request — headers, body, raw bytes — searchable by timestamp, by hook URL, by provider. The Pro plan at $19/month adds replay: take that original captured payload and send it to any target URL. Not to a configured destination — to any URL, including localhost, including a staging environment, including a debugging endpoint that logs everything.

When you are building your idempotency store against Hookdeck event IDs, HookTunnel gives you the ground truth those IDs point to. You can cross-reference: Hookdeck shows this event was delivered three times; HookTunnel shows what the payload contained on each delivery. If the payload was identical across retries (it should be), the idempotency store should have caught it. If it wasn't identical — you have found something interesting.


The Architecture Lesson

The best webhook infrastructure forces you to be honest about your assumptions — and Hookdeck does this by making idempotency non-negotiable.

Hookdeck forces at-least-once discipline. By documenting the guarantee explicitly, by giving you the event ID header for deduplication, by retrying 50 times without apology — Hookdeck is telling you that duplicates are real and your handler must be ready for them. Teams that internalize this build more resilient systems. The discipline transfers: once you are writing idempotent handlers for Hookdeck, you write idempotent handlers for everything.

Svix forces the same discipline on your customers' behalf. If you are the sender, your customers need idempotent handlers too. Svix's retry engine means your customers will see the same event multiple times. The idempotency conversation is the same one, one step downstream.

HookTunnel sits at the foundation of that conversation: the captured payload history is the ground truth against which idempotency claims are verified. When you are building that deduplication store, you are storing event IDs and outcomes. When something goes wrong, you need to know what those event IDs corresponded to. That is what a history of every payload gives you.

The idempotency store is the mechanism. The event ID is the key. The captured payload is the evidence. Three distinct tools, three distinct jobs — and the incident that exposes which one you are missing.


The Honest Conclusion

Hookdeck's at-least-once guarantee is valuable because it is specific. A vague promise to "retry on failure" tells you nothing about what to build. An explicit at-least-once contract tells you exactly what to build: an idempotency store, atomic claims, and a deduplication handle.

That is genuinely good engineering guidance embedded in a product decision. The teams that use Hookdeck and understand why it works the way it does come out with better systems than the teams that just plug it in and hope.

And if you are building that idempotency store — if you care enough about the payload to dedup it by ID and store the processing result — you are also going to want a history of what was actually in those payloads. That is where the new dog comes in.

Stop guessing. Start proving.

Generate a webhook URL in one click. No signup required.

Get started free →