Semaphore
[Semaphore] is the limit-greater-than-1 form of Warp's concurrency primitive. It's an alias of [Mutex] over the same IWarpSemaphoreProvider — the only differences from [Mutex] are that you supply a slot count and the default mode is Wait (queue surplus) instead of Skip (drop surplus).
Use [Semaphore] when you want to cap N concurrent jobs per key. Use [Mutex] when you want at most one.
Setup
Same addon as Mutex — opt.AddConcurrency() registers both:
builder.Services.AddWarpServer<AppDbContext>(opt =>
{
opt.UsePostgreSql();
opt.AddConcurrency();
});
Usage
Static slot count via attribute:
[Semaphore("payment-api", limit: 5)]
public class CallPaymentApi : IJob { }
Or set it dynamically per-enqueue:
await publisher.Enqueue(
new CallPaymentApi(),
new JobParameters().WithSemaphore("payment-api", limit: 5));
Default mode is Wait
[Semaphore] defaults to ConcurrencyMode.Wait — surplus jobs are requeued (State = Enqueued, ScheduleTime = now) and re-attempt the slot on the next pickup. This matches the standard semaphore semantic ("queue, don't drop").
Override to Skip if you want surplus jobs cancelled instead:
[Semaphore("payment-api", limit: 5, Mode = ConcurrencyMode.Skip)]
public class DropOnFull : IJob { }
Contract or handler?
[Semaphore] can sit on the job/message type (the default for everything that runs it — shared by all handlers of a message), on a job/message handler class (that handler only), or on both, in which case the handler wins. [Mutex] and [Semaphore] are one family, so a handler [Semaphore] overrides a contract [Mutex] outright. The resolved policy is written onto the job row at first execution.
Precedence
WithSemaphore(...) / WithMutex(...) // passed at enqueue, highest priority
→ [Semaphore] / [Mutex] // on the handler class
→ [Semaphore] / [Mutex] // on the job/message type
There is no global default — a concurrency policy without a key is not a policy — and a rung supplies the
key, limit and mode together, never a field of one. An IConcurrencyLimitManager admin row sits above
all of it for the limit only (see Admin overrides); it never changes which
key a job contends on. See Where do I declare the policy?.
Related
For full details on modes, the admin-override layer, the Mutex vs Semaphore namespace split on shared keys, dashboard integration, and edge cases, see Concurrency control. That page is the canonical reference for both attributes.