TypeScript SDK v4 is now available! See what's new
Event Coordination

Running functions in parallel

Fan out to multiple model calls, evaluations, or processing tasks from a single event.

Event-driven systems let you have multiple subscribers for a single event stream. This allows you to fan out#Message-oriented_middleware) an event to multiple functions that run in parallel, each retrying independently on failure.

This is different from how most queueing systems work. In systems like SQS or Celery, a specific job in a queue runs a single function. This can cause developers to bundle unrelated logic into a single background job, where a failure in one part causes unrelated code to fail too.

Fan-out is especially useful for AI workloads. A single event can trigger multiple model calls, each running independently: one to summarize, another to classify, another to extract entities. If the classification call fails, it retries without re-running the summarization. You can also fan out to run the same prompt against multiple models for evaluation, or distribute a batch of tasks across parallel workers.

§How this works

Inngest allows you to create as many functions as you need which subscribe to the same event:

typescript
01import { inngest } from "./client";
02
03// Summarize the content
04const summarize = inngest.createFunction(
05 { id: "summarize-content", triggers: [{ event: "document/uploaded" }] },
06 async ({ event, step }) => {
07 const content = await step.run("fetch-content", async () => {
08 return await storage.getContent(event.data.documentId);
09 });
10 return await step.run("generate-summary", async () => {
11 return await llm.summarize(content);
12 });
13 }
14);
15
16// Classify the document type
17const classify = inngest.createFunction(
18 { id: "classify-document", triggers: [{ event: "document/uploaded" }] },
19 async ({ event, step }) => {
20 const content = await step.run("fetch-content", async () => {
21 return await storage.getContent(event.data.documentId);
22 });
23 return await step.run("classify", async () => {
24 return await llm.classify(content, {
25 categories: ["invoice", "contract", "report"],
26 });
27 });
28 }
29);
30
31// Extract key entities
32const extractEntities = inngest.createFunction(
33 { id: "extract-entities", triggers: [{ event: "document/uploaded" }] },
34 async ({ event, step }) => {
35 const content = await step.run("fetch-content", async () => {
36 return await storage.getContent(event.data.documentId);
37 });
38 return await step.run("extract", async () => {
39 return await llm.extract(content, {
40 fields: ["names", "dates", "amounts", "companies"],
41 });
42 });
43 }
44);

In this example, all three functions run automatically in parallel whenever the document/uploaded event is received. Each runs independently and retries on its own. If the classification model returns an error, only that function retries. The summarization and entity extraction continue unaffected.

You can also fan out within a single function using step.run() calls, but separating into distinct functions gives you independent retry policies, independent concurrency limits, and cleaner observability in the dashboard.

§Alternative approaches

You can build an event-driven system using NATS, Redis, or Kafka, all of which are reliable event streaming components. You'll need to configure topics, manage stateful subscribers with always-alive services, handle ack/nack behavior, manage retries and dead-letter queues, and maintain watermarks yourself. It's a common approach but requires significant setup and ongoing maintenance.

§Additional resources