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:

Install the shim package
npm install @resonatehq/aws

Instead of importing Resonate from the SDK package, import it from the AWS shim package.

index.ts
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:

Install the shim package
npm install @resonatehq/cloudflare

Instead of importing Resonate from the SDK package, import it from the Cloudflare shim package.

index.ts
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:

Google Cloud Functions#

Google Cloud Functions worker shim | TypeScript

Quick example:

Install the shim package
npm install @resonatehq/gcp

Instead of importing Resonate from the SDK package, import it from the GCP shim package.

index.ts
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:

Install the shim package
npm install @resonatehq/supabase

Instead of importing Resonate from the SDK package, import it from the Supabase shim package.

index.ts
import { Resonate } from "@resonatehq/supabase";
import { countdown } from "./count";

const resonate = new Resonate();
resonate.register("countdown", countdown);

resonate.httpHandler();

Ready to use example apps:

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#

PlatformMax invocation timeoutNotes
AWS Lambda15 minCheck @resonatehq/aws for default ttl
Google Cloud Functions Gen 29 min (540s)@resonatehq/gcp defaults ttl to 5 min — bump to 10 min so the lease always exceeds the function timeout
Google Cloud Run60 minCheck @resonatehq/gcp for default ttl
Cloudflare Workersvaries (CPU-time vs wall-clock differ by plan)See Cloudflare Workers limits
Supabase Edge FunctionsvariesSee Supabase Edge Functions limits
Avoid forever loops in a single durable invocation

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.

code
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):

code
import { Resonate } from "@resonatehq/gcp";

const resonate = new Resonate({ ttl: 10 * 60 * 1000 }); // 10 min

ttl 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: