tutorial

Self-Hosting Inngest: A Background Task Manager for AI Agents

inngestself-hostinginfrastructure
npx skills add joelhooks/joelclaw --skill inngest-local

Your agent needs a way to do things that take longer than a single tool call. Download a video, transcribe it, summarize it, file it — that’s four steps, each of which can fail independently. Inngest gives you durable functions where each step is memoized and retried. Self-hosted, zero cloud dependencies, running on your Mac.

# Generate signing keys (one-time)
INNGEST_SIGNING_KEY="signkey-dev-$(openssl rand -hex 16)"
INNGEST_EVENT_KEY="evtkey-dev-$(openssl rand -hex 16)"
 
# Save them — your worker needs these too
echo "INNGEST_SIGNING_KEY=$INNGEST_SIGNING_KEY" >> .env.inngest
echo "INNGEST_EVENT_KEY=$INNGEST_EVENT_KEY" >> .env.inngest
 
# Start the server (30 seconds to dashboard)
docker run -d --name inngest \
  -p 8288:8288 \
  -v inngest-data:/var/lib/inngest \
  -e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
  -e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
  --restart unless-stopped \
  inngest/inngest:latest \
  inngest start --host 0.0.0.0
 
# Open the dashboard
open http://localhost:8288

One Docker container. Dashboard at localhost:8288. That’s the whole infrastructure.

⚠️ Signing keys are mandatory as of inngest/inngest:latest (Feb 2026). Without INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY, the container crash-loops with Error: signing-key is required. The keys above use openssl rand — any random hex string works for local dev.

For agents — Inngest published official skills that cover setup, events, steps, flow control, middleware, and durable functions. Install all six:

npx skills add inngest/inngest-skills --yes --global

Or cherry-pick what you need:

npx skills add inngest/inngest-skills --skill inngest-setup --yes --global
npx skills add inngest/inngest-skills --skill inngest-steps --yes --global
npx skills add inngest/inngest-skills --skill inngest-durable-functions --yes --global
npx skills add inngest/inngest-skills --skill inngest-flow-control --yes --global
npx skills add inngest/inngest-skills --skill inngest-events --yes --global
npx skills add inngest/inngest-skills --skill inngest-middleware --yes --global

Once installed, your agent knows how to set up Inngest, write durable functions with proper step patterns, configure concurrency/throttling/debounce, and handle errors with retries — without you explaining any of it.


Why

You know this failure mode.

A script dies at minute 27 and you rerun the whole thing. An agent loop hums for 30 minutes and you keep the terminal open like a life support machine.I lost an 8-story coding loop this way. Agent was on story 6 of 8. Closed the lid, machine slept, loop died. The first 5 stories were committed but the retry logic didn’t exist yet. A video download succeeds, transcription crashes, and you lose the good part.

Durable workflows fix all of these. Each step in a function retries independently. The machine reboots, the job picks up where it left off. Self-hosting means your data stays on your machine.Inngest Cloud exists and is great. I self-host because the data includes code diffs, transcripts, and agent conversations I don’t want leaving my network.

What it is

Inngest is an event-driven workflow engine. You send an event, a function runs, that function is broken into steps. If step 2 fails, step 1 does not run again.

Self-hosted = one Docker container (the server) + your worker process (Bun/Node). Dashboard at localhost:8288 shows every event, every step, every retry.

What happens when you set this up

docker run → Inngest server + dashboard (localhost:8288)
bun run   → your worker registers functions
curl      → send events, functions execute durably
launchd   → everything survives reboots

What I run on this

Fourteen functions. Two pipelines. The full architecture is in Inngest is the Nervous System.

Video ingest pipeline: YouTube URL → yt-dlp download → NAS archive → mlx-whisper transcription → vault note → AI enrichment. Five steps across three tools. Any step retries independently.

Autonomous coding loops: A PRD goes in, committed code comes out. Planner → Implementor → Reviewer → Judge → Retrospective. Each role is a separate function run with its own retry policy and timeout.

Heartbeat cron: Every 15 minutes, prunes stale sessions, audits trigger registrations, pushes a gateway event. The gateway picks it up.

Patterns that matter

Event chaining — function A emits an event that triggers function B. No orchestrator holding state. Inngest replays from the last completed step if anything crashes.

Claim-check — step outputs have a size limit. Write large data to a file, pass the path. The transcribe step writes a 1MB transcript to /tmp, returns just the filepath.

Concurrency keysconcurrency: { key: "event.data.project", limit: 1 }. One coding loop per project. One transcription at a time (GPU saturation).mlx-whisper saturates the Apple Silicon GPU. Running two transcriptions concurrently doesn’t error — it just makes both take 3x longer. Concurrency limit of 1 is the right call. Multiple downloads in parallel.

Gotchas I hit

Missing peer dependency. bun add inngest doesn’t pull @inngest/ai, but the SDK imports it at startup. Worker crashes with Cannot find module '@inngest/ai'. Install both:

bun add inngest @inngest/ai

Docker-to-host networking. If Inngest runs in Docker and your worker runs on the host (bare metal Bun), the server can’t reach localhost:3111 — that’s the container’s localhost, not yours. Use host.docker.internal:This is Docker Desktop/OrbStack-specific. Colima and Lima also support it. Linux Docker needs --add-host=host.docker.internal:host-gateway on the container.

# When starting the worker, tell Inngest where to find it
INNGEST_DEV=1 bun run src/serve.ts
# The Inngest server needs --sdk-url pointing back to host
# Dashboard → Apps → manually set SDK URL to http://host.docker.internal:3111/api/inngest

Or pass it on the docker run:

docker run -d --name inngest \
  ... \
  inngest/inngest:latest \
  inngest start --host 0.0.0.0 \
  --sdk-url http://host.docker.internal:3111/api/inngest

The k8s naming collision. A Service named inngest creates an INNGEST_PORT env var. The Inngest binary also uses INNGEST_PORT — expects an integer, gets tcp://10.43.x.x:8288.Kubernetes auto-creates {SERVICE_NAME}_PORT env vars for every Service. If your Service name collides with an env var the binary expects, you get silent misconfig. Name your Service inngest-svc or similar. This is a rite of passage. Two characters: name it inngest-svc. Thirty minutes of debugging.

Trigger drift. Functions register triggers at startup. Server state can drift from code. Triggers silently break. I built an auditor that runs every 15 minutes and alerts on drift.

Worker re-registration. After Inngest server restart, the worker needs to re-register. joelclaw refresh or just restart the worker process.

For humans

The deeper architecture narrative — why each decision was made, what the alternatives were, how the pieces compose — is in Inngest is the Nervous System and The One Where Joel Deploys Kubernetes… Again.


This is a living document. Updated as the system evolves.