alepha@docs:~/framework/docs/guides/features$
cat 8-background-tasks.md | pretty
2 min read
Last commit:

#Background Tasks

alepha/background runs a task without making the caller wait for it: a welcome email, a webhook, a write that does not have to finish first. It is one call, defer(), and it works the same on Node and on Cloudflare Workers.

typescript
1import { BackgroundTaskProvider } from "alepha/background";

#Deferring a task

typescript
 1import { $inject } from "alepha"; 2import { BackgroundTaskProvider } from "alepha/background"; 3import { $action, okSchema } from "alepha/server"; 4  5class Signup { 6  protected readonly background = $inject(BackgroundTaskProvider); 7  8  register = $action({ 9    method: "POST",10    path: "/signup",11    schema: { response: okSchema },12    handler: async () => {13      this.background.defer(() => this.sendWelcomeEmail());14      return { ok: true };15    },16  });17 18  protected async sendWelcomeEmail() {19    console.log("sent");20  }21}

defer() returns immediately and the task starts on a microtask, at the handler's next await, so often before the response is sent. A throw inside it never reaches the handler. It shares the thread with everything else, so CPU work in a task still blocks until it yields.

#What it does not do

A deferred task is a promise in this process. It has no record anywhere.

  • It is not retried. A task that fails is logged as a warning and is gone.
  • It does not survive a crash. A process that dies takes its running tasks with it.
  • It runs on this instance, and nothing limits how many run at once.

For anything that has to happen, use a job.

You need Use
Work the response should not wait for, and that may be lost defer()
Work that must happen, with retries $job
Work on a schedule $job with a cron
An event several listeners react to $topic

#Runtimes

Runtime What keeps the task alive
Node, Bun The event loop.
Cloudflare Workers The request's waitUntil, because the isolate is frozen once the response is out.

On Workers the build swaps in WorkerdBackgroundTaskProvider, and the call site does not change.

⚠️ waitUntil belongs to one invocation, so on Workers call defer() inside its async context, as a handler or a cron tick does. Outside one, the task still runs, without the guarantee that the isolate waits for it.

#Stopping and testing

Tasks in flight are tracked. On stop the app waits for them, within the 8 seconds it gives all its stop hooks, and past that it gives up. A task deferred by another task during shutdown is not waited for. flush() waits for what is running at any other time, and never rejects:

typescript
 1import { Alepha } from "alepha"; 2import { BackgroundTaskProvider } from "alepha/background"; 3  4const alepha = Alepha.create(); 5const background = alepha.inject(BackgroundTaskProvider); 6await alepha.start(); 7  8let sent = false; 9background.defer(async () => {10  sent = true;11});12 13await background.flush();14console.log(sent); // true