Issue Description
Webhook event IDs appear to be generated from only:
- event timestamp in seconds
- event type id
Relevant code:
crates/trc/src/serializers/json.rs
map.serialize_entry(
"id",
&format!("{}{}", event.inner.timestamp, event.inner.typ.to_id()),
)?;
EventDetails does not currently appear to store a per-event unique id:
crates/trc/src/lib.rs
pub struct EventDetails {
pub typ: EventType,
pub timestamp: u64,
pub level: Level,
pub span: Option<Arc<Event<EventDetails>>>,
}
This means two distinct events with the same event type emitted in the same second can serialize with the same webhook id.
What “event type” means here
An event is one actual thing that happened.
The event type is the category of that thing.
Examples from Stalwart telemetry include:
queue.message-queueddelivery.delivereddelivery.failedjmap.method-callsmtp.rcpt-totelemetry.webhook-errorserver.startup
Two different emails being queued are two different events, but they can both have the same event type: queue.message-queued.
Expected Behavior
Webhook event id should identify a unique event occurrence.
Two distinct events should not share an id, even if they have:
- the same event type
- the same second-level timestamp
Actual Behavior
Two same-type events emitted in the same second can produce the same webhook id.
Example:
event A: timestamp=1750000000, type=queue.message-queued -> id derived from "1750000000" + queue-message-queued type id
event B: timestamp=1750000000, type=queue.message-queued -> id derived from "1750000000" + queue-message-queued type id
Since both events have the same timestamp second and same event type, the serialized webhook id can be identical.
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
PostgreSQL
Blob Storage
PostgreSQL
Search Engine
Internal
Directory Backend
Internal
Additional Context
Webhook consumers commonly use the event id for idempotency and deduplication.
This matters because webhook delivery retries the full batch on non-2xx responses.
Relevant code:
crates/common/src/telemetry/webhooks/mod.rs
if let Err(err) = post_webhook_events(&settings, &wrapper).await {
trc::event!(Telemetry(TelemetryEvent::WebhookError), Details = err);
if webhook_tx.send(wrapper.events.into_inner()).await.is_err() {
trc::event!(
Server(ServerEvent::ThreadError),
Details = "Failed to send failed webhook events back to main thread",
CausedBy = trc::location!()
);
}
}
Full-batch retry is an at-least-once delivery model. Under that model, consumers need reliable event IDs to safely deduplicate retried events.
If two distinct events share the same webhook id, a consumer may incorrectly treat the second event as a duplicate and skip it.
Example payload shape:
[
{
"id": "1750000000123",
"createdAt": "2025-06-15T10:30:15Z",
"type": "queue.message-queued",
"data": {
"queueId": "email-a"
}
},
{
"id": "1750000000123",
"createdAt": "2025-06-15T10:30:15Z",
"type": "queue.message-queued",
"data": {
"queueId": "email-b"
}
}
]
Those are two different events, but the duplicated id makes idempotent processing unsafe.
### I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
### I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
### I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I'd prefer a human-only reply, I'll add the no-ai tag to my topic.
on