# Parallel steps with Task.WhenAll

> Run steps at the same time with Task.WhenAll and Task.WhenAny, how Inngest executes them, and what to watch out for.

Web page: https://damikun.github.io/inngest-dotnet/guides/parallel-steps/

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

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

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

Figure: When a call reaches two new steps at once, the SDK runs neither. It answers 206 with both as planned steps, Inngest runs each in a call of its own, possibly at the same time and on different instances, and calls the function again once both results are saved.

[How it works](https://damikun.github.io/inngest-dotnet/how-it-works.md) 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

Start one step per item and await them together. Give each step an id of its own:

```csharp
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](https://damikun.github.io/inngest-dotnet/guides/flow-control.md#concurrency),
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](https://damikun.github.io/inngest-dotnet/guides/invoking-functions.md) 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](https://damikun.github.io/inngest-dotnet/guides/fan-out.md).

## Racing steps

`Task.WhenAny` works too. It completes with the step that finished first:

```csharp
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:

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

- **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()` and `GetAwaiter().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)` or `Task.Run` inside 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 `DbContext` that the function injects. Within one call the function's code and
  its steps do share it; [Scoped steps](https://damikun.github.io/inngest-dotnet/guides/scoped-steps.md) 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](https://www.inngest.com/docs/durable-execution/limits) page.

The Inngest docs describe [parallel steps](https://www.inngest.com/docs/durable-execution/primitives/group-parallel)
for the TypeScript SDK, which uses `Promise.all` where .NET uses `Task.WhenAll`.

## Related examples

[Fan-out with invoke](https://damikun.github.io/inngest-dotnet/examples/fan-out-with-invoke.md) and the [Connect worker](https://damikun.github.io/inngest-dotnet/examples/connect-worker.md) run steps in parallel.
