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