Serverless workers
Deploy workers to serverless platforms (Lambda, Cloud Functions, etc).
Currently only the TypeScript SDK supports serverless worker shims.
What is a serverless worker shim?#
A serverless worker shim, also referred to as a binding, is a lightweight adapter that allows Resonate Workers to run in serverless (FaaS) environments such as Cloudflare Workers, AWS Lambda, Google Cloud Functions, etc. The shim handles the communication between the Resonate SDK and the serverless platform, allowing you to run long-running workflows in ephemeral serverless functions with relative ease.
Supported platforms#
The following serverless platforms are supported with official Resonate worker shims:
AWS Lambda#
Amazon Lambda worker shim | TypeScript
Quick example:
npm install @resonatehq/awsInstead of importing Resonate from the SDK package, import it from the AWS shim package.
import { Resonate } from "@resonatehq/aws";
import { countdown } from "./count";
const resonate = new Resonate();
resonate.register("countdown", countdown);
export const handler = resonate.httpHandler();Cloudflare Workers#
Cloudflare Workers worker shim | TypeScript
Quick example:
npm install @resonatehq/cloudflareInstead of importing Resonate from the SDK package, import it from the Cloudflare shim package.
import { Resonate } from "@resonatehq/cloudflare";
import { countdown } from "./count";
const resonate = new Resonate();
resonate.register("countdown", countdown);
export default resonate.handlerHttp();Ready to use example apps:
- Durable Counter with Cloudflare Workers | TypeScript
- Deep Research Agent with Cloudflare Workers | TypeScript
Google Cloud Functions#
Google Cloud Functions worker shim | TypeScript
Quick example:
npm install @resonatehq/gcpInstead of importing Resonate from the SDK package, import it from the GCP shim package.
import { Resonate } from "@resonatehq/gcp";
import { countdown } from "./count";
const resonate = new Resonate();
resonate.register("countdown", countdown);
export const handler = resonate.handlerHttp();Ready to use example apps:
Supabase Edge Functions#
Supabase Edge Functions worker shim | TypeScript
Quick example:
npm install @resonatehq/supabaseInstead of importing Resonate from the SDK package, import it from the Supabase shim package.
import { Resonate } from "@resonatehq/supabase";
import { countdown } from "./count";
const resonate = new Resonate();
resonate.register("countdown", countdown);
resonate.httpHandler();Ready to use example apps:
- Durable Counter with Supabase Edge Functions | TypeScript
- Deep Research Agent with Supabase Edge Functions | TypeScript
Serverless-specific constraints#
Serverless workers run inside platform-imposed invocation timeouts. Resonate's acquired-task lease (ttl) interacts with that timeout: the lease is the window the server gives the worker to make progress on a task before it considers the task abandoned and reassigns it. Workflows that accumulate many promises in a single root will eventually fail to replay within the lease, even if individual steps are fast.
Platform timeouts#
| Platform | Max invocation timeout | Notes |
|---|---|---|
| AWS Lambda | 15 min | Check @resonatehq/aws for default ttl |
| Google Cloud Functions Gen 2 | 9 min (540s) | @resonatehq/gcp defaults ttl to 5 min — bump to 10 min so the lease always exceeds the function timeout |
| Google Cloud Run | 60 min | Check @resonatehq/gcp for default ttl |
| Cloudflare Workers | varies (CPU-time vs wall-clock differ by plan) | See Cloudflare Workers limits |
| Supabase Edge Functions | varies | See Supabase Edge Functions limits |
A workflow that loops forever inside one root promise — while(true) { doWork() } — accumulates child promises with every iteration. After thousands of iterations (10k+ in practice), replay duration exceeds the acquired-task lease, the server emits error code 1199 — Task is not acquired and reassigns the task mid-execution, and cadence collapses. The longer the workflow runs, the worse it gets.
The fix: recursive tail call with ctx.detached#
Have the per-iteration function play exactly one iteration, and as its last yield, spawn the next iteration as a brand-new root via ctx.detached. The current invocation returns; the next invocation starts with a fresh origin id and an empty history.
function* playGame(ctx: Context, n: number) {
// ...play exactly one game (ctx.run, ctx.sleep, etc.)...
yield* ctx.detached(playGame, n + 1); // ✅ last yield, fresh root, bounded replay
}The split must happen inside the per-iteration function. A parent loop that calls ctx.detached repeatedly — for (;;) yield* ctx.detached(work, n) — still records each call in the parent's history, reproducing the bug on the parent. See Bound the promise count of a single execution for the full explanation.
Configuring ttl#
The shim Resonate constructor accepts a ttl option (milliseconds):
import { Resonate } from "@resonatehq/gcp";
const resonate = new Resonate({ ttl: 10 * 60 * 1000 }); // 10 minttl should exceed the platform's max invocation timeout. The lease tells the server how long to wait before assuming the worker is dead. If ttl is shorter than the platform timeout, the server may reassign the task to a fresh invocation while the original is still legitimately running, leading to mid-execution task reassignment (error code 1199) and duplicate work.
See also: