hooksentinel
Errors

duplicate_event

hooksentinel error duplicate_event — the event ID was already marked processed by the idempotency store. This is expected behavior, not a failure.

Summary

FieldValue
Codeduplicate_event
HTTP status200
RetryableNo

What caused it

The idempotency store's has() check found this event ID already marked processed, so hooksentinel skipped onEvent and returned a 200 immediately. This is the deduplication feature working as intended, not an error condition — it's included in the error code list because it's still a distinct outcome your onError handler can observe, not because something went wrong.

Providers deliver at least once by design. A checkout.session.completed event can arrive twice because:

  • Your server didn't respond within the provider's timeout window on the first delivery (even if onEvent actually succeeded — the ack just arrived too late).
  • The provider's own retry logic redelivers after a transient failure on their end.
  • You manually resent a test event from a provider's dashboard.

Returning 200 for a duplicate (rather than an error status) matters: if hooksentinel returned a 4xx or 5xx for a duplicate, the provider would interpret that as "still failing" and keep retrying indefinitely, when the event was in fact already handled successfully the first time.

When to actually investigate

A steady low rate of duplicates is normal and doesn't need action. Investigate if you see:

  • A high duplicate rate for one specific event type. This usually means onEvent for that type is slow enough to regularly miss the provider's ack timeout — see the BullMQ fast-ack pattern to move slow work off the request path.
  • Duplicates appearing immediately after a deploy. If you're using memoryStore(), a process restart clears all dedup state — every in-flight or recently-processed event looks "new" again after a redeploy. Switch to redisStore() or prismaStore() for anything beyond local development; see Idempotency.

Observing duplicates

onError: async (error, ctx) => {
  if (error.code === 'duplicate_event') {
    metrics.increment('webhook.duplicate', { provider: ctx.provider });
  }
},

Nothing else needs to be done here by default — hooksentinel already returns the correct 200 response.

Last updated on

On this page