Durable workers on Google Cloud Run

A Resonate workflow running on Cloud Functions Gen 2 — every sleep checkpoints, every restart resumes.

Cloud Run Workers banner

A countdown workflow running as a single Google Cloud Function. The function sleeps between notifications via ctx.sleep, terminating the container while suspended. Resonate fires the function back up at the right time and the workflow continues from the next iteration — long-running logic on short-lived compute.

SDK versions

TypeScript: @resonatehq/sdk v0.10.1 + @resonatehq/gcp for the Cloud Functions adapter. Python and Rust adapters are forthcoming.

The problem#

Cloud Functions and Cloud Run give you scale-to-zero and rapid horizontal scaling, but the runtime is stateless and execution-time-bounded. Long-running business logic — a 30-day trial expiry, a multi-step approval flow, a polling loop — doesn't fit the shape: hold a process open for hours and you're paying for idle compute, restart and you've lost your place.

The conventional escape is to chain a serverless trigger to a database to a scheduler to another trigger, with bespoke compensation when any link fails. That's a lot of glue per workflow.

Resonate's solution#

Write the workflow as a normal generator function. ctx.sleep checkpoints the wake-up time on the Resonate Server and suspends the workflow — the Cloud Function exits cleanly. When the sleep elapses, Resonate dispatches a fresh function invocation that resumes the workflow from the next line. The same applies to any ctx.run or ctx.rpc: every yield is a suspension point that can survive a cold start.

Code walkthrough#

The whole workflow is a generator. The Cloud Function entry point is one line: register the function and expose Resonate's HTTP handler.

The countdown workflow#

src/count.ts
import type { Context } from "@resonatehq/sdk";

export function* countdown(
  ctx: Context,
  count: number,
  delay: number,
  url: string,
) {
  for (let i = count; i > 0; i--) {
    // Send a notification.
    yield* ctx.run(notify, url, `Countdown: ${i}`);
    // Sleep is a suspension point — the Cloud Function exits, then resumes.
    yield* ctx.sleep(delay * 60 * 1000);
  }
  yield* ctx.run(notify, url, "Done");
}

async function notify(_ctx: Context, url: string, msg: string) {
  await fetch(url, { method: "POST", body: msg, headers: { "Content-Type": "text/plain" } });
}

The Cloud Function entry point#

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

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

export const handler = resonate.handlerHttp();

@resonatehq/gcp wraps the SDK with a Cloud Functions adapter. handlerHttp() returns the HTTP handler the Functions runtime expects. Each invocation drives the workflow forward by one step (one ctx.run or one ctx.sleep), then exits.

Run it locally#

code
git clone https://github.com/resonatehq-examples/example-countdown-gcp-ts
cd example-countdown-gcp-ts
npm install
Terminal 1 — Resonate Server
brew install resonatehq/tap/resonate
resonate dev
Terminal 2 — Cloud Function in local dev mode
npm run dev

The Cloud Function listens on http://localhost:8080. Pick a unique ntfy.sh topic name and start a countdown via the Resonate CLI:

code
resonate invoke countdown.1 \
  --func countdown \
  --arg 5 --arg 1 --arg "https://ntfy.sh/<your-topic>" \
  --target "http://localhost:8080"

Open the ntfy.sh topic in your browser. Notifications arrive once per minute. Now kill the local Function process mid-countdown — the workflow resumes from the next iteration when you restart it.

Try the deploy story#

Deploy the Resonate Server to Cloud Run, deploy the countdown as a Cloud Function (Gen 2), and trigger it via the Resonate CLI. The workflow can sleep for days between notifications without paying for idle compute — Cloud Functions only spin up to advance the workflow one step at a time. The repo's README walks through the full gcloud run deploy and gcloud functions deploy commands.

  • Lambda workers — same suspend-and-resume pattern on AWS.
  • Durable sleep — the underlying primitive, focused on a non-serverless worker.