Blog
Written by the Erasure product and engineering team
Part of Data deletion
Webhook Retries and Idempotency for Deletion Workflows
When a deletion job depends on webhooks, retries are guaranteed and duplicates are certain. An idempotency key and a signature check are what keep the system safe.
The moment your deletion workflow touches a webhook, two facts take over: the delivery will fail sometimes, and the retry will arrive more than once. If your receiver is not idempotent, the second delivery is not a duplicate—it is a second deletion event hitting a system that already acted. This is how you build a receiver that survives retries.
Retries are not a bug
Webhooks are delivered over HTTP with no transaction. The sender cannot know whether the receiver processed the event or crashed after storing it, so it retries. That means your receiver must be able to process the same event twice with the same result. That property has a name: idempotency.
The standard mechanism is an idempotency key. Every event carries a unique ID, and the receiver records which IDs it has already processed:
-- Track processed events so retries are no-ops.
CREATE TABLE webhook_events_processed (
event_id TEXT PRIMARY KEY,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
On each delivery, check the ID first. If it exists, return a success response without doing the work again:
INSERT INTO webhook_events_processed (event_id)
VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING processed_at;
If the insert returns a row, this is the first time—do the work. If it returns nothing, it is a retry—acknowledge and stop. The uniqueness constraint makes the race safe even if two deliveries arrive at once.
Verify the signature before you trust the payload
Any webhook endpoint on the public internet will receive forged payloads. Verify the signature before doing anything else.
For HMAC-signed webhooks the pattern is consistent: compute the HMAC over the raw body with the shared secret and compare against the header, using a constant-time comparison so timing does not leak the secret:
import crypto from "node:crypto";
function validSignature(rawBody: string, signature: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
Two rules that matter: hash the raw body, not a re-parsed version, or the signature will not match; and never log the signing secret or put it in client-side code.
The receiver ordering that keeps you safe
Do the checks in the right order:
- Verify the signature. Reject anything that does not match.
- Check the event ID against the processed table. Return success if already handled.
- Process the work (delete, notify, enqueue).
- Record the event ID in the same transaction as the work, if possible, so a crash between "did the work" and "recorded the ID" still leads to a safe retry.
The worst failure is doing the work, failing to record the ID, then crashing—because the retry runs the deletion twice. Doing the work and recording the ID in one transaction closes that gap.
A webhook is not a delete
A webhook tells another system "this user asked to be deleted." It does not delete anything itself. If the receiving system is down, retries are limited, and the event is dropped, the deletion silently never happens.
For anything that must actually be erased, prefer a direct delete path (a database connector or an API call your job controls) and use the webhook only to notify systems you cannot reach inbound. Treat the webhook as a notification with a best-effort delivery, not as the source of truth for erasure.
What durable deletion looks like
The robust shape is a job queue, not a pile of HTTP calls. A job is persisted, claimed by a worker, retried with backoff on failure, and its outcome recorded. If the process dies, the job survives and resumes. That is the difference between "we fired a webhook" and "we ran a deletion and can prove it."
This is the model Erasure uses: durable deletion jobs run by a worker, with retries and honest outcomes, and a signed webhook system for notifying systems it cannot reach directly. The consent side also delivers signed consent.updated webhooks with an HMAC signature you verify on the raw body.
The data deletion hub has the broader workflow, and the deletion requests guide covers the full loop from intake to evidence.
About this post
Written by the Erasure product and engineering team
Published 6 August 2026
Part of Data deletion
This article is grounded in Erasure's product documentation and explains engineering and operational implications. Where it discusses regulation, it is not legal advice. See our editorial policy.
More on data deletion
Deleting a User's Data from MySQL: Dependency Order and Transactions
MySQL has its own traps when you delete one user's data—no deferrable foreign keys, the FOREIGN_KEY_CHECKS footgun, and InnoDB locking. Here is the safe order.
How to Delete a User's Data from PostgreSQL Without Breaking the Database
Deleting one user's data from Postgres means finding every table that references them, handling foreign keys in the right order, and running a safe parameterized DELETE. Here is the working method.
Soft Delete vs Hard Delete for Privacy Requests
A soft-deleted row is still data. For privacy deletion requests, the flag that hides it is not the same as the deletion a regulator can see. Here is when each makes sense.