Background tasks
Run heavy or slow work outside the request/response cycle: a request returns immediately and the work happens later, in the background, with retries and de-duplication. Use it for anything that would blow the request budget or that should survive the response: processing an upload, calling an external API, an ingestion pipeline, sending a batch of emails.
A task runs in an ephemeral isolate: the platform loads your app's code, runs one handler, and tears it down. The app does not have to be serving HTTP for a task to run.
Declare handlers with withTasks
Wrap your default HTTP handler with withTasks and pass a map of named task handlers. The default
export stays your request handler; onvibe reads the task handlers from it.
import { enqueueTask, withTasks } from "./.onvibe/helpers.ts";
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/import") {
const { fileId } = await req.json();
const { taskId } = await enqueueTask("process_import", { fileId }, {
dedupKey: `import:${fileId}`,
});
return Response.json({ taskId }, { status: 202 });
}
return new Response("hello");
}
export default withTasks(handler, {
// Each handler gets the payload you passed to enqueueTask. Keep it IDEMPOTENT:
// a task can run more than once on retry; the source of truth is your database.
process_import: async ({ fileId }) => {
const res = await fetch(`https://api.example.com/files/${fileId}`); // external fetch is allowed
// ...heavy work, write results to your Postgres...
return { processed: true };
},
});
Enqueue work with enqueueTask
const { taskId, deduplicated } = await enqueueTask("process_import", { fileId }, {
dedupKey: "import:42", // optional: won't enqueue a second task while one with this key is active
maxAttempts: 3, // optional: retries on failure (exponential backoff)
});
enqueueTask(name, data?, opts?)returns{ taskId, deduplicated }.namemust match a handler registered inwithTasks.- De-dup is scoped to your app:
dedupKeynever collides with other apps. - Enqueue from anywhere in your handler — a request, a cron, or even another task.
Check status with checkTask
const status = await checkTask(taskId);
// null if no such task, else:
// { status: "pending" | "running" | "done" | "failed", result?: unknown, error?: string }
if (status?.status === "done") {
// status.result is whatever the task handler returned
}
Notes
- Idempotency is your job: a task may run more than once (retries, timeouts). Make handlers safe to re-run; keep the real state in your database.
- Retries: failures retry with exponential backoff up to
maxAttempts(default 3), then the task isfailed(its error is incheckTask().error). - External network: task handlers can
fetchthe public internet (RSS, third-party APIs, LLMs). - Timeouts: a task that runs too long is considered stuck and retried.
- Tasks vs cron: cron fires your normal handler on a schedule (see scheduled tasks); tasks
run a
withTaskshandler you enqueue on demand, off the request path.