Skip to content
Get started
Get started
Deduplication and Idempotent Job Adds
guide · queue

The same job, only once.

Retried HTTP calls, at-least-once webhooks and impatient users all produce duplicate adds. A custom id or a dedup key makes the second one a no-op instead of a second charge.

Give a job a custom jobId and adding it twice does nothing: while a generation with that ID is live (waiting, delayed, prioritized, waiting-children, or active), the existing job is returned instead of creating a duplicate. Custom IDs are broker-wide because they are also the global persisted job primary key, so this remains idempotent even if the second add targets another queue. Once the prior generation is terminal, the ID may be reused; bunqueue retires its completed/DLQ state before admitting exactly one fresh generation. This makes add() safe to call repeatedly in embedded and TCP modes.

const job1 = await queue.add('process', { orderId: 123 }, { jobId: 'order-123' });
const job2 = await queue.add('process', { orderId: 123 }, { jobId: 'order-123' });
console.log(job1.id === job2.id); // true, same job returned

Typical uses: webhook retries, double-submits from a UI, restoring jobs on service startup without duplicating them.

The deduplication option dedupes within a time window instead of permanently. The id field is required:

// Same id within 1 hour = no new job. After the TTL, a new job is allowed.
await queue.add('notification', { userId: '123' }, {
deduplication: { id: 'notify-123', ttl: 3600000 }
});

Two strategies change what happens when a duplicate arrives:

// extend: keep the existing job, reset its TTL (debouncing, "keep quiet while active")
await queue.add('sync-task', { action: 'sync' }, {
deduplication: { id: 'sync-task', ttl: 60000, extend: true }
});
// replace: remove the pending job, insert a new one with the latest data (last write wins)
await queue.add('latest-data', { data: newData }, {
deduplication: { id: 'data-job', ttl: 300000, replace: true }
});
OptionTypeDefaultDescription
idstring(required)Unique deduplication key
ttlnumber-Time in ms before the key expires
extendbooleanfalseReset TTL on duplicate, keep existing job
replacebooleanfalseRemove pending job, create a new one (new internal id)

Managing keys directly in Bun, or retaining an explicit custom job ID in the network SDKs when later lookup is required:

const jobId = await queue.getDeduplicationJobId('my-unique-key'); // look up
await queue.removeDeduplicationKey('my-unique-key'); // allow re-adding
const job = await queue.getJob(jobId!);
const removed = await job?.removeDeduplicationKey(); // only if this generation owns it

The Bun queue lookup/removal methods work in embedded and TCP modes. Job-level removal is generation-safe: a stale job cannot clear a key already transferred to a replacement job. Direct TTL deduplication-key lookup and removal are currently Bun-only. A custom jobId is a separate value: network SDKs can look it up, but that query never resolves deduplication.id.

Queue APICreate a queue in embedded or TCP mode
Adding Jobsadd, addBulk, priorities, delays, durability
Querying JobsFetch jobs, states, counts and results
Queue Control and MaintenancePause, drain, obliterate, clean and repair
Progress, Job Logs and DependenciesProgress, per-job logs and dependencies
Queue Rate Limiting and Global ConcurrencyRate limits and global concurrency caps
Job Schedulers from the QueueNamed repeatable schedules from the queue
DLQ Operations from the Queue ObjectFailed-job operations from the Queue object
Workers, Stats and Metrics from the QueueRegistered workers, stats and metrics windows
Namespaces, Auto-Batching and Store-and-ForwardNamespaces, auto-batching, store-and-forward
JobOptions ReferenceEvery JobOptions field, with defaults