A concurrency key is a string you attach to a job that guarantees at most one job with that key is processing at a time. If a second job with the same key is dispatched while the first is still running, it waits — it gets delayed 10 seconds and retried automatically.

This is the n8n equivalent of "don't run this workflow if it's already running for this contact."

Typical use case

You're running contact enrichment. You fan out 500 jobs — one per contact. Without concurrency keys, if the same contact appears in two lists, two enrichment jobs can run simultaneously, causing duplicate API calls and conflicting writes.

import { Rotor } from "@rotorsh/sdk";
 
const rotor = new Rotor({ apiKey: process.env.ROTOR_API_KEY! });
 
await rotor.jobs.enqueue("enrichment", {
  payload: { contactId: "cid_123" },
  concurrencyKey: "contact:cid_123",
});

Any subsequent job with concurrencyKey: "contact:cid_123" will wait until the first completes or fails.

How it works

  1. When a job is dispatched to your callback URL, Rotor attempts to acquire a Redis lock: SET NX EX 300 {workspace}:ckey:{concurrencyKey} {jobId}.
  2. If the lock is free, the job proceeds normally. The lock is released when the job completes or fails.
  3. If the lock is held (another job with the same key is in flight), the current job is delayed by 10 seconds and Rotor retries it automatically. This repeats until the lock is free.
  4. A 300-second safety-net TTL ensures the lock is always released, even if the worker crashes mid-job.

Concurrency keys are scoped to your workspace — there is no cross-workspace collision.

Batch enqueue

const contacts = ["cid_001", "cid_002", "cid_003"];
 
await rotor.jobs.enqueueBatch("enrichment",
  contacts.map((id) => ({
    payload: { contactId: id },
    concurrencyKey: `contact:${id}`,
  }))
);

Each contact gets its own lock. Two contacts can enrich in parallel; the same contact cannot.

REST API

curl -X POST "https://api.rotor.sh/v1/queues/enrichment/jobs" \
  -H "Authorization: Bearer $ROTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": { "contactId": "cid_123" },
    "concurrencyKey": "contact:cid_123"
  }'

Key naming

Any string up to 256 characters. Common patterns:

PatternExample
Entity IDcontact:cid_123
Account + actionaccount:acc_456:sync
User + resourceuser:usr_789:report
Warning

Concurrency keys only apply to callback-mode queues (queues with a callback_url). They have no effect on durable workflow jobs.

Combining with tags

You can use both on the same job:

await rotor.jobs.enqueue("enrichment", {
  payload: { contactId: "cid_123" },
  tags: ["campaign:q2-outbound", "contact:cid_123"],
  concurrencyKey: "contact:cid_123",
});

Tags are for filtering and visibility; the concurrency key is for mutual exclusion. They serve different purposes.