Skip to content

Concurrency, throttling and rate limits

Flow control decides when runs start and how many run at once. You set it per function, in its FunctionOptions or with attributes on its class, the SDK sends it to Inngest when the app syncs, and Inngest enforces it. Nothing in your app has to queue, lock or count.

SyncAccount.cs
using Inngest;
public sealed record AccountUpdated(string AccountId) : IInngestEventData
{
public static string EventName => "account/updated";
}
public sealed record AccountDeleted(string AccountId) : IInngestEventData
{
public static string EventName => "account/deleted";
}
public sealed class SyncAccount : InngestFunction<AccountUpdated>
{
public override Task<object?> ExecuteAsync(FunctionContext<AccountUpdated> context, CancellationToken cancellationToken) =>
Task.FromResult<object?>(null);
}

Its options, with a concurrency limit, a throttle, a debounce and a cancellation event:

[InngestFunction("sync-account")]
[EventTrigger<AccountUpdated>]
[Concurrency(5, Key = "event.data.accountId")]
[Throttle(10, "1m")]
[Debounce("30s", Key = "event.data.accountId")]
[CancelOn<AccountDeleted>(Match = "data.accountId")]

The attributes go on SyncAccount, and inngest.AddFunction<SyncAccount>() registers it without options.

Each attribute sets the FunctionOptions member of the same name. Durations are TimeSpans in code and time strings in attributes, such as "30s", "5m" or "1h30m", because attribute arguments must be constants. The sections below show both forms. The function options builder shows the JSON the options turn into, and which record each attribute sets.

The timeline below sends the same burst of events to a function with each kind of flow control; change the settings to see which runs start at once, which wait, which are skipped and which are merged. The flow control explainer says how to read it.

Flow control

Concurrency

At most a number of runs execute at the same time. Others wait for a free slot and start in order. Concurrency in the Inngest docs.

Settings
Timeline of events and runs15 events, 15 runs, 13 delayed by up to 20s.eventsruns0s10s20s30s40s50s60s70s
15 events, 15 runs, 13 delayed by up to 20s.started at oncedelayedskippedmerged into a later run
The numbers
Event arrivesOutcomeIts run starts
0sstarted at once0s
1sstarted at once1s
1.5sdelayed8s
2sdelayed9s
3sdelayed16s
4sdelayed17s
10sdelayed24s
11sdelayed25s
12sdelayed32s
13sdelayed33s
25sdelayed40s
26sdelayed41s
40sdelayed48s
41sdelayed49s
42sdelayed56s

Throttle

At most a number of runs start per period. Others are queued and start later, spread out evenly. Throttle in the Inngest docs.

Settings
Timeline of events and runs15 events, 15 runs, 13 delayed by up to 17.0s.eventsruns0s10s20s30s40s50s60s
15 events, 15 runs, 13 delayed by up to 17.0s.started at oncedelayedskippedmerged into a later run
The numbers
Event arrivesOutcomeIts run starts
0sstarted at once0s
1sdelayed3.3s
1.5sdelayed6.7s
2sdelayed10s
3sdelayed13.3s
4sdelayed16.7s
10sdelayed20s
11sdelayed23.3s
12sdelayed26.7s
13sdelayed30.0s
25sdelayed33.3s
26sdelayed36.7s
40sstarted at once40s
41sdelayed43.3s
42sdelayed46.7s

Rate limit

At most a number of runs start per period. Events beyond that are skipped and never run. Rate limit in the Inngest docs.

Settings
Timeline of events and runs15 events, 11 runs, 4 skipped.eventsruns0s10s20s30s40s50s60s
15 events, 11 runs, 4 skipped.started at oncedelayedskippedmerged into a later run
The numbers
Event arrivesOutcomeIts run starts
0sstarted at once0s
1sstarted at once1s
1.5sstarted at once1.5s
2sskippednever
3sskippednever
4sstarted at once4s
10sstarted at once10s
11sstarted at once11s
12sskippednever
13sskippednever
25sstarted at once25s
26sstarted at once26s
40sstarted at once40s
41sstarted at once41s
42sstarted at once42s

Debounce

One run starts once events stop arriving for a period, with the last event. A timeout caps the wait. Debounce in the Inngest docs.

Settings
Timeline of events and runs15 events, 4 runs, 11 merged into a later event's run.eventsruns0s10s20s30s40s50s60s
15 events, 4 runs, 11 merged into a later event's run.started at oncedelayedskippedmerged into a later run
The numbers
Event arrivesOutcomeIts run starts
0smerged into a later run9s
1smerged into a later run9s
1.5smerged into a later run9s
2smerged into a later run9s
3smerged into a later run9s
4sstarted at once9s
10smerged into a later run18s
11smerged into a later run18s
12smerged into a later run18s
13sstarted at once18s
25smerged into a later run31s
26sstarted at once31s
40smerged into a later run47s
41smerged into a later run47s
42sstarted at once47s

Most options take a Key, an expression evaluated against each event. Runs with the same key value share a limit, and runs with different values do not. A concurrency limit of 1 keyed on event.data.accountId runs one step at a time per account, while all accounts run side by side. A key can combine fields: event.data.tenantId + "-" + event.data.region.

[Concurrency(50)]
[Concurrency(1, Key = "event.data.accountId")]

Limits how many steps of the function execute at the same time. Runs over the limit wait in a queue and start in order. A function can have up to two limits; a step starts when it fits both. Concurrency counts executing steps, not runs: a run that sleeps or waits for an event holds no slot. A limit of 0 means no limit, as in Inngest.

Scope widens a keyed limit beyond the function. ConcurrencyScope.Environment shares it with every function in the environment that uses the same key, and ConcurrencyScope.Account with every environment of the account. Both need a key. See concurrency.

[Throttle(10, "1m", Burst = 5, Key = "event.data.merchantId")]

Limits how many runs start per period. Runs over the limit are queued, not dropped, and their starts are spread over the period. Burst lets that many extra runs start at once. Use it to protect an API with a rate limit of its own. The period must be at least one second. See throttling.

[RateLimit(1, "4h", Key = "event.data.userId")]

Also limits how many runs start per period, but events over the limit are skipped and never run. Use it where running late is worse than not running, such as a “your report is ready” email. The period is between one second and 24 hours. See rate limiting.

[Debounce("30s", Key = "event.data.productId", Timeout = "5m")]

Waits until events stop arriving for the period, then runs once with the last event. Each new event restarts the period. Timeout caps the total wait, so a steady stream of events still runs the function every so often. The period is between one second and seven days. See debounce.

[BatchEvents(100, "10s", Key = "event.data.importId")]

Runs the function with up to MaxSize events at once, or with what has arrived when the timeout passes. The events are in context.Events; context.Event is the first of them. If limits batching to events that match an expression; others run on their own.

var batch = context.Events.Select(e => e.Data).ToList();
await context.Step.RunAsync("write-rows", ct => rows.WriteAsync(batch, ct));

MaxSize must be at least 2 and the timeout at least one second; how long it can be depends on your Inngest plan, which the SDK leaves to Inngest. Inngest does not allow batching together with cancellation, debounce, singletons or priority, and the SDK rejects those combinations at startup. See batching.

[Priority("event.data.plan == 'enterprise' ? 120 : 0")]

Run is an expression that returns a number of seconds, from -600 to 600. A run with priority 120 starts ahead of runs that have been waiting for up to two minutes; a negative value holds a run back. Priority only matters when runs queue up, so it goes together with concurrency or throttling. See priority.

[InngestFunction("record-payment", Idempotency = "event.data.paymentId")]

Starts at most one run per value of the expression in 24 hours, for this function. Other functions triggered by the same event are not affected. Inngest implements it as a rate limit of 1 per 24 hours, so it replaces any RateLimit. To deduplicate an event for every function, give the event an Id instead. See idempotency.

[Singleton(SingletonMode.Skip, Key = "event.data.accountId")]

Allows one active run per key. With SingletonMode.Skip a new run is skipped while another is active; with SingletonMode.Cancel the new run cancels the active one. Unlike a concurrency limit of 1, this covers the whole run, sleeps and waits included. See singleton.

[CancelOn<AccountDeleted>(Match = "data.accountId")]
[CancelOn("subscription/changed", If = "async.data.plan == 'free'", Timeout = "7d")]

Cancels a run when a matching event arrives. Match compares a property of the triggering event with the same property of the cancelling one; If is a full expression where event is the triggering event and async the cancelling one; when both are set, both apply. CancelOn.For<T>() and [CancelOn<T>] take the event’s name from a typed event. Timeout limits how long after the run is scheduled an event can still cancel it. A function can have up to five.

Cancellation takes effect between steps: a step that is running finishes, and the run stops before the next one. See cancellation.

[Timeouts(Start = "10m", Finish = "1h")]

Start cancels a run that waited in the queue longer than that before its first step. Finish cancels a run that takes longer than that from its first step to the end. Neither interrupts a step that is running; give the calls inside your steps timeouts of their own. See timeouts.

The SDK checks these options when the app starts, against the limits Inngest enforces, whether they come from code or attributes. It throws one InvalidOperationException that lists every problem of every function, each naming its function, for example Function "import-rows": BatchEvents cannot be combined with Debounce. A duration in an attribute that is not a time string fails as well, when AddFunction reads it. A mistake fails the deploy instead of the sync.

The flow control overview in the Inngest docs explains how the options interact and in which order Inngest applies them.

The webhook pipeline uses idempotency and a throttle, nightly billing concurrency and a singleton, and order fulfillment cancellation.

inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.