# Concurrency, throttling and rate limits

> Concurrency, throttling, rate limits, debounce, batching, priority, idempotency, singletons, cancellation and timeouts, set in FunctionOptions or with attributes and enforced by Inngest.

Web page: https://damikun.github.io/inngest-dotnet/guides/flow-control/

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.

```csharp title="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:

## Attributes

```csharp
[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.

## Code first

```csharp
inngest.AddFunction<SyncAccount>(new FunctionOptions
{
    Id = "sync-account",
    Triggers = [Trigger.Event<AccountUpdated>()],
    Concurrency = [new ConcurrencyLimit(5) { Key = "event.data.accountId" }],
    Throttle = new Throttle(10, TimeSpan.FromMinutes(1)),
    Debounce = new Debounce(TimeSpan.FromSeconds(30)) { Key = "event.data.accountId" },
    CancelOn = [CancelOn.For<AccountDeleted>() with { Match = "data.accountId" }],
});
```

Each attribute sets the `FunctionOptions` member of the same name. Durations are `TimeSpan`s 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](https://damikun.github.io/inngest-dotnet/explainers/function-options.md) shows the JSON
the options turn into, and which record each attribute sets.

## See it on a timeline

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](https://damikun.github.io/inngest-dotnet/explainers/flow-control.md) says how to read it.

An interactive version is on [the web page](https://damikun.github.io/inngest-dotnet/guides/flow-control/).

## Keys

Most options take a `Key`, an [expression](https://www.inngest.com/docs/guides/writing-expressions) 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

### Attributes

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

### Code first

```csharp
Concurrency =
[
    new ConcurrencyLimit(50),
    new ConcurrencyLimit(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](https://www.inngest.com/docs/durable-execution/flow-control/concurrency).

## Throttle

### Attributes

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

### Code first

```csharp
Throttle = new Throttle(10, TimeSpan.FromMinutes(1)) { 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](https://www.inngest.com/docs/durable-execution/flow-control/throttling).

## Rate limit

### Attributes

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

### Code first

```csharp
RateLimit = new RateLimit(1, TimeSpan.FromHours(4)) { 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](https://www.inngest.com/docs/durable-execution/flow-control/rate-limiting).

## Debounce

### Attributes

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

### Code first

```csharp
Debounce = new Debounce(TimeSpan.FromSeconds(30)) { Key = "event.data.productId", Timeout = TimeSpan.FromMinutes(5) },
```

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](https://www.inngest.com/docs/durable-execution/flow-control/debounce).

## Batching

### Attributes

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

### Code first

```csharp
BatchEvents = new BatchEvents(100, TimeSpan.FromSeconds(10)) { 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.

```csharp
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](https://www.inngest.com/docs/durable-execution/flow-control/batching).

## Priority

### Attributes

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

### Code first

```csharp
Priority = new 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](https://www.inngest.com/docs/durable-execution/flow-control/priority).

## Idempotency

### Attributes

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

### Code first

```csharp
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`](https://damikun.github.io/inngest-dotnet/guides/sending-events.md#deduplicating-with-an-id) instead. See
[idempotency](https://www.inngest.com/docs/durable-execution/guides-and-advanced/idempotency).

## Singleton

### Attributes

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

### Code first

```csharp
Singleton = new 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](https://www.inngest.com/docs/durable-execution/flow-control/singleton).

## Cancel on events

### Attributes

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

### Code first

```csharp
CancelOn =
[
    CancelOn.For<AccountDeleted>() with { Match = "data.accountId" },
    new CancelOn("subscription/changed") { If = "async.data.plan == 'free'", Timeout = TimeSpan.FromDays(7) },
],
```

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](https://damikun.github.io/inngest-dotnet/guides/sending-events.md#typed-events). `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](https://www.inngest.com/docs/durable-execution/guides-and-advanced/cancellation/events).

## Timeouts

### Attributes

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

### Code first

```csharp
Timeouts = new FunctionTimeouts { Start = TimeSpan.FromMinutes(10), Finish = TimeSpan.FromHours(1) },
```

`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](https://www.inngest.com/docs/durable-execution/guides-and-advanced/cancellation/timeouts).

## Validation

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](https://www.inngest.com/docs/durable-execution/flow-control) in the Inngest docs explains
how the options interact and in which order Inngest applies them.

## Related examples

The [webhook pipeline](https://damikun.github.io/inngest-dotnet/examples/webhook-pipeline.md) uses idempotency and a throttle, [nightly billing](https://damikun.github.io/inngest-dotnet/examples/nightly-billing.md) concurrency and a singleton, and [order fulfillment](https://damikun.github.io/inngest-dotnet/examples/order-fulfillment.md) cancellation.
