Parallel steps with Task.WhenAll
Steps run in parallel when the function starts them before awaiting them. There is no special API: start the
tasks, then await them with Task.WhenAll or Task.WhenAny.
var user = context.Event.Data;
var profile = context.Step.RunAsync("load-profile", ct => profiles.LoadAsync(user.UserId, ct));var orders = context.Step.RunAsync("load-orders", ct => orderHistory.LoadAsync(user.UserId, ct));await Task.WhenAll(profile, orders);
context.Logger.LogInformation("{Name} has {Count} orders", (await profile).Name, (await orders).Count);How Inngest runs them
Section titled “How Inngest runs them”When a call finds more than one new step, the SDK runs none of them. It answers with a StepPlanned op for each,
and Inngest runs every planned step in a call of its own. Those calls can run at the same time, on one instance of
the app or on several. Once the steps have finished, Inngest calls the function again; the results are memoized
and the function continues after Task.WhenAll.
Call 1 reaches two new steps
load-profile and load-orders, before Task.WhenAll
Call 2 runs
load-profile
Call 3 runs
load-orders
Call 4 continues after Task.WhenAll
both steps return their saved results
How it works shows a parallel section call by call, including the planned steps and the calls that run them.
When a call finds a single new step, the SDK runs it right away, as it does for sequential steps, unless Inngest has asked it not to while a parallel section is still running.
Fan-out over a list
Section titled “Fan-out over a list”Start one step per item and await them together. Give each step an id of its own:
var invoices = await context.Step.RunAsync("load-invoices", ct => billing.GetOpenInvoicesAsync(customerId, ct));
var paymentIds = await Task.WhenAll(invoices.Select(invoice => context.Step.RunAsync($"charge-{invoice.Id}", ct => billing.ChargeAsync(invoice.Id, ct))));Every step counts toward the function’s concurrency limit, so a limit of 5 runs at most five of these charges at once, across all runs of the function. For hundreds of items, consider invoking a function per item, or sending an event per item, so that each item is a run of its own with its own retries.
MapAsync does this for a list, with ids for the steps, a limit on how many run at once, and a choice of stopping at
the first failure or collecting every outcome, and its chunked form sends lists that are too long for one run to child
runs. See Fan-out over a list.
Racing steps
Section titled “Racing steps”Task.WhenAny works too. It completes with the step that finished first:
var primary = context.Step.RunAsync("quote-primary", ct => primaryCarrier.QuoteAsync(shipment, ct));var backup = context.Step.RunAsync("quote-backup", ct => backupCarrier.QuoteAsync(shipment, ct));
var quote = await await Task.WhenAny(primary, backup);Every planned step still runs to the end; the losing quote is recorded and simply not used. By default Inngest calls the function again only after all the planned steps have finished, so a race picks the first result but does not return it any sooner. The SDK replays results in the order they finished, so the same step wins on every later call.
To go on as soon as the first step finishes, run the race in race mode. Every step started inside ParallelAsync
tells Inngest to call the function again after each of them finishes:
var quote = await await context.Step.ParallelAsync(ParallelMode.Race, () => Task.WhenAny( context.Step.RunAsync("quote-primary", ct => primaryCarrier.QuoteAsync(shipment, ct)), context.Step.RunAsync("quote-backup", ct => backupCarrier.QuoteAsync(shipment, ct))));The losing step still runs to the end. The first step after the race runs right away, but the steps after that wait for the losing step to finish, unless they run in race mode too.
Things to watch
Section titled “Things to watch”- Different ids in different branches. Two parallel branches that reuse an id share its numbering, and which branch gets which result can depend on timing. The SDK logs a warning when it sees that.
- Do not block on a step.
.Result,.Wait()andGetAwaiter().GetResult()on a step’s task never return: the SDK hands out results on the function’s own scheduler, which a blocked thread holds up. - Leaving the scheduler. Code that uses
ConfigureAwait(false)orTask.Runinside a function still works, but steps started from other threads may be reported one at a time instead of all at once. - Unawaited steps still run. A step that is started and never awaited is reported to Inngest like the others. If the function returns while such steps are new, the SDK reports the steps instead of the output and returns the output on a later call.
- Scoped services. Each step started together runs in a call of its own, with a dependency injection scope of its
own, so parallel steps do not share a
DbContextthat the function injects. Within one call the function’s code and its steps do share it; Scoped steps gives a step a scope of its own. - Inngest’s limits. A run has 1,000 steps by default, parallel ones included, and fails once that many have completed. See the limits page.
The Inngest docs describe parallel steps
for the TypeScript SDK, which uses Promise.all where .NET uses Task.WhenAll.
Related examples
Section titled “Related examples”Fan-out with invoke and the Connect worker run steps in parallel.
inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.