rust 61 lines · 8 steps

Idempotent webhook handling in Axum

A shared, time-bounded set of seen event IDs lets an Axum handler drop duplicate webhook deliveries safely across concurrent requests.

Explained by highlit
1use axum::{
2 extract::State,
3 http::StatusCode,
4 Json,
5};
6use serde::Deserialize;
7use std::{
8 collections::HashMap,
9 sync::Arc,
10 time::{Duration, Instant},
11};
12use tokio::sync::Mutex;
13 
14#[derive(Clone)]
15pub struct WebhookDedup {
16 seen: Arc<Mutex<HashMap<String, Instant>>>,
17 ttl: Duration,
18}
19 
20impl WebhookDedup {
21 pub fn new(ttl: Duration) -> Self {
22 Self {
23 seen: Arc::new(Mutex::new(HashMap::new())),
24 ttl,
25 }
26 }
27 
28 async fn check_and_mark(&self, event_id: &str) -> bool {
29 let now = Instant::now();
30 let mut seen = self.seen.lock().await;
31 seen.retain(|_, ts| now.duration_since(*ts) < self.ttl);
32 if seen.contains_key(event_id) {
33 return false;
34 }
35 seen.insert(event_id.to_owned(), now);
36 true
37 }
38}
39 
40#[derive(Deserialize)]
41pub struct WebhookEvent {
42 id: String,
43 #[serde(rename = "type")]
44 kind: String,
45 payload: serde_json::Value,
46}
47 
48pub async fn handle_webhook(
49 State(dedup): State<WebhookDedup>,
50 Json(event): Json<WebhookEvent>,
51) -> StatusCode {
52 if !dedup.check_and_mark(&event.id).await {
53 tracing::info!(event_id = %event.id, "skipping duplicate webhook delivery");
54 return StatusCode::OK;
55 }
56 
57 tracing::info!(event_id = %event.id, kind = %event.kind, "processing webhook");
58 ProcessWebhook::enqueue(event.kind, event.payload).await;
59 
60 StatusCode::ACCEPTED
61}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Webhook providers retry deliveries, so handlers must be idempotent to avoid processing the same event twice.
  2. 2Wrapping shared state in Arc<Mutex<...>> lets Axum clone it per request while serializing access to the map.
  3. 3Pruning expired entries on every check keeps the dedup set bounded without a separate cleanup task.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent webhook handling in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code