Deduplicate webhook deliveries with KV Store
Skip a webhook event your function already handled, with one key per event ID in KV Store, written only after the work succeeds.
You mark each webhook event that a function handled with a key in KV Store, and the function skips an event whose key already exists, from the Azion API and the function’s code. To read and write keys for other purposes, refer to Manage key-value data from a function.
A provider that does not see a delivery acknowledged sends the same event again, so one event can reach the function more than once. A key per event ID lets the function recognize a repeat and answer it without doing the work twice.
- The function reads the key for the event ID. A value means an earlier delivery of the event was handled, so the function answers
200and does nothing else. - Without a key, the function does the work the event triggers.
- When the work fails, the function answers
500and writes no key, so the provider’s next delivery runs the work again. - When the work succeeds, the function writes the key with an expiration and answers
200.
Prerequisites
- KV Store enabled on your account. The product is in Preview and is not enabled by default, so request access through Technical Support.
- A personal token for the API call that creates the namespace.
- A function that receives the provider’s webhook and verifies its signature, such as the handler that Build a Stripe webhook handler with Functions builds. Each event the provider sends carries an ID that stays the same across its deliveries.
The examples use the namespace webhook-events, keys of the form event:<event-id>, and an expiration of 86400 seconds. Replace them with your own values.
Create the namespace
A namespace is created through the Azion API, and a function only opens one that already exists. A namespace cannot be renamed or deleted, and two names that differ only in case are two namespaces, so create it once, in lowercase. A name runs 3 to 63 characters of letters, numbers, the hyphen, and the underscore.
To create the namespace, send its name to the KV Store API:
The API answers 201 with the namespace. The request is synchronous, and there is no provisioning state to poll:
The webhook-events namespace exists and is empty. For every field and error of the namespace API, refer to Namespaces.
In the Build event-driven APIs use case, this section takes these values:
| Setting | Value |
|---|---|
| Namespace | events-idempotency |
A namespace cannot be renamed or deleted, and names are case-sensitive, so create it once, in lowercase. The events-idempotency namespace exists and is empty.
In the Build AI agents use case, this section takes these values:
| Setting | Value |
|---|---|
| KV Store namespace | agent-state |
Skip a delivery the function already handled
kv.get returns null for a key the namespace does not hold, so a null means the event is new. The function writes the key only after the work succeeds: a key written before a failed attempt marks the event as handled, and the provider’s retry is then skipped. expirationTtl removes the key after the seconds it names, so the namespace does not keep a key per event forever. Its minimum is 60 seconds.
To deduplicate deliveries, add this code to the function, after the signature check:
Azion.KV is a global of the runtime, reached with no import line and no credential. Azion.KV.open throws NotFound: KV namespace "webhook-events" does not exist when the account holds no namespace with that name. Under azion dev, open accepts a name that belongs to no namespace, so test the function once it is deployed.
The first delivery of an event runs handleEvent and writes event:<event-id>. A later delivery of the same event, within one day, answers {"received":true,"duplicate":true} and does not run the work. A repeat that arrives after the key expires runs the work again, so set expirationTtl longer than the time your provider keeps retrying an event.
Each delivery reads one key, and each new event writes one. KV Store includes 100,000 keys read and 1,000 keys written per day before a charge applies, and it accepts 1 write per second to the same key. For every bound, refer to KV Store limits.
Publish each event to Upstash QStash once
When the work an event triggers is a publish to a queue, the key is written only after the queue accepts the message. The route in this section replaces the POST /webhook route of the handler that Build a Stripe webhook handler with Functions builds, through its step 4, and keeps its verifyStripeWebhook middleware. It reads the namespace events-idempotency, created as Create the namespace shows, and the QStash token and the consumer’s domain from the QSTASH_TOKEN and CONSUMER_DOMAIN environment variables, stored as Store the token and the destination shows.
In the entry file of the project, replace the app.post("/webhook", ...) route with this one:
Deploy the project from its directory:
Azion CLI builds the project, deploys it, and opens Azion Console on the page that carries the deployment logs. The webhook endpoint registered with the provider stays https://api.example.com/webhook. Propagation takes a few minutes.
The route answers the provider as soon as the event is queued. A repeated delivery answers {"received":true,"duplicate":true} and publishes nothing, and a publish that QStash does not accept answers 500, so the provider retries the event.
To confirm the route, send a test event with the Stripe CLI, then read what the function logged:
The handler answers { "received": true }, and the Stripe Dashboard lists the delivery and its response code for the endpoint. The terminal prints the console messages of the last 5 minutes. No QStash answered line appears, so QStash accepted every publish.
The Build event-driven APIs use case uses the values of this example.
These checks confirm the Build event-driven APIs use case.