Webhook event IDs collide for same event type emitted in the same second

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-queued
  • delivery.delivered
  • delivery.failed
  • jmap.method-call
  • smtp.rcpt-to
  • telemetry.webhook-error
  • server.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

Let me look at the relevant source code to verify the report before responding.

The user’s analysis is accurate. Looking at the actual source code in crates/trc/src/serializers/json.rs (line ~86):

map.serialize_entry(
    "id",
    &format!("{}{}", event.inner.timestamp, event.inner.typ.to_id()),
)?;

The webhook event id is indeed constructed from only the event’s second-level timestamp and the event type’s numeric ID. EventDetails carries no per-event unique identifier beyond those two fields, so two distinct events of the same type emitted within the same second will produce an identical id string.

This is a confirmed bug in the current codebase. The fix would need to either:

  1. Add a unique identifier (e.g. an atomic counter, UUID, or higher-resolution timestamp) to EventDetails, and include it in the serialized id, or
  2. Incorporate a sequence number or hash of additional event data into the id at serialization time.

Until this is fixed, webhook consumers relying on id for idempotency/deduplication should be aware that collisions are possible and may need to use a combination of id + event position within the batch as a workaround.

This is a genuine bug report with clear reproduction logic and a well-identified root cause. A human maintainer will need to triage and address it.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

This has been fixed. The fix will be included in v0.16.11.