Background worker with Inngest Connect
This example splits an app in two: a public web app that accepts uploads and sends events, and a worker in a private network that processes them. The worker has no inbound port at all. It connects out to Inngest with Connect and receives its work over that connection.
The events
Section titled “The events”The web app sends ImageUploaded, which triggers the worker, and the worker announces ThumbnailsReady when it is
done. The records live in a small class library that both projects reference, so the name and the data of an event are
written once. The library needs only the Inngest package:
using Inngest;
public sealed record ImageUploaded(string ImageId, string BlobPath) : IInngestEventData{ public static string EventName => "media/image.uploaded";}
public sealed record ThumbnailsReady(string ImageId, string[] Paths) : IInngestEventData{ public static string EventName => "media/thumbnails.ready";}The worker
Section titled “The worker”using Inngest;
var builder = Host.CreateApplicationBuilder(args);
// Register your IThumbnailer implementation here; any image library works.builder.Services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromMinutes(2));builder.Services .AddInngest(options => options.AppId = "media-worker") .AddFunction<MakeThumbnails>() .AddConnect(connect => { // Stable per container, unique between containers. connect.InstanceId = Environment.GetEnvironmentVariable("HOSTNAME"); connect.MaxWorkerConcurrency = 4; });
await builder.Build().RunAsync();using Inngest;
public sealed class MakeThumbnails(IThumbnailer thumbnailer) : InngestFunction<ImageUploaded>{ private static readonly int[] Widths = [160, 480, 1200];
public override async Task<object?> ExecuteAsync(FunctionContext<ImageUploaded> context, CancellationToken cancellationToken) { var image = context.Event.Data;
var paths = await Task.WhenAll(Widths.Select(width => context.Step.RunAsync($"resize-{width}", ct => thumbnailer.ResizeAsync(image.BlobPath, width, ct))));
await context.Step.SendEventAsync("announce", new ThumbnailsReady(image.ImageId, paths));
return new { image.ImageId, thumbnails = paths.Length }; }}
public interface IThumbnailer{ Task<string> ResizeAsync(string blobPath, int width, CancellationToken cancellationToken);}The function’s options go on MakeThumbnails as attributes: its id, its trigger and a concurrency limit of 8.
[InngestFunction("make-thumbnails")][EventTrigger<ImageUploaded>][Concurrency(8)]Worker/Program.cs registers it with .AddFunction<MakeThumbnails>(), without options.
Without the attributes, Worker/Program.cs registers the function with its options in code: its id, its trigger and
a concurrency limit of 8.
inngest.AddFunction<MakeThumbnails>(new FunctionOptions{ Id = "make-thumbnails", Triggers = [Trigger.Event<ImageUploaded>()], Concurrency = [new ConcurrencyLimit(8)],});The worker’s project uses the worker SDK, references the contracts library and adds the Inngest.Connect package, which
brings in Inngest:
<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Inngest.Connect" Version="*" /> <ProjectReference Include="../Contracts/Contracts.csproj" /> </ItemGroup></Project>The web app
Section titled “The web app”The web app only sends events, so it needs AddInngest, an event key and the contracts library, but no functions and
no MapInngest:
using Inngest;
var builder = WebApplication.CreateBuilder(args);builder.Services.AddInngest(options => options.AppId = "media-web");
var app = builder.Build();
app.MapPost("/images/{imageId}", async (string imageId, IInngestClient inngest, CancellationToken cancellationToken) =>{ // Store the upload first; then hand the processing to the worker. var blobPath = $"uploads/{imageId}"; await inngest.SendAsync(new ImageUploaded(imageId, blobPath), cancellationToken); return Results.Accepted();});
app.Run();Running it in containers
Section titled “Running it in containers”FROM mcr.microsoft.com/dotnet/sdk:10.0 AS buildWORKDIR /srcCOPY . .RUN dotnet publish Worker/Worker.csproj -c Release -o /app
FROM mcr.microsoft.com/dotnet/runtime:10.0WORKDIR /appCOPY --from=build /app .USER $APP_UIDENTRYPOINT ["dotnet", "Worker.dll"]The worker image needs only the .NET runtime, not ASP.NET Core, and exposes no port. Give it the signing key to
connect, and the event key because it sends ThumbnailsReady; the web app needs the event key:
docker run -e INNGEST_SIGNING_KEY=signkey-prod-... -e INNGEST_EVENT_KEY=... media-worker:latestdocker run -p 8080:8080 -e INNGEST_EVENT_KEY=... media-web:latestHow it works
Section titled “How it works”- No ingress. The worker dials Inngest; nothing dials the worker. It works from a private subnet, a laptop or a network that only allows outbound traffic.
- Two apps in Inngest. The worker syncs itself as
media-workerthrough its connection. The web app has no functions to sync; it only sends events. - Capacity per worker.
MaxWorkerConcurrencycaps how many calls Inngest sends one worker at a time, and Inngest spreads the rest over other workers. The function’s own concurrency limit caps all workers together. - Graceful shutdown. When the container stops, the worker takes no new work and waits up to two minutes for running calls. Calls cut off after that are cancelled and retried elsewhere. Set the orchestrator’s grace period a little longer than the shutdown timeout.
- Several workers. Run as many containers as you need. Each needs its own
InstanceId, which the container’s host name provides.
inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.