A consumer that is idempotent by construction, not by check

A deduplication table is a check bolted on afterwards, and some operations are naturally repeatable — which is better, because there is nothing to get wrong.

// not idempotent: running twice doubles the count
DB::table('stats')->increment('views');

// idempotent by construction: the same event sets the same value
DB::table('order_status')->updateOrInsert(
    ['order_id' => $e->orderId],
    ['status' => $e->status, 'updated_at' => $e->occurredAt]
);

// and the version check, for out-of-order delivery
where('version', '<', $e->version)

Designing the operation to be a set rather than an increment removes the need for any deduplication at all, and it is available more often than people expect. The version check handles the harder case: at-least-once delivery says nothing about order, so a stale event arriving after a newer one will overwrite it unless something compares. Together they cover both failure modes with no extra table.