5 min readResonate HQJust published

Embedding Resonate inside Flask, FastAPI, and Django handlers in Python

How a Python webserver route invokes a registered function under durable execution and reads its result without standing up a separate worker process.

Resonate brand card on a dark background with a plum spectrum wave at the bottom and the post headline in white Sansation.

A Python webserver needs to run a registered function under durable execution in response to an HTTP request and return the result to the caller. Resonate lets the route handler invoke the registered function by promise id, wait for the result, and respond — the function runs as a durable promise inside the same Python process, with each step checkpointed. This example shows the same three-function call shape wired into Flask, FastAPI, and Django, so the integration shape is visible across all three frameworks.

The shape of the solution

The Flask variant is the smallest. The other two share the same call shape; only the framework glue and the data flowing through the functions differ (Flask returns the string "hello world!" through foo → bar → baz; FastAPI and Django each return 1 from baz and add + 1 at every parent level, so foo returns 3).

from flask import Flask, jsonify
from resonate import Resonate
 
 
app = Flask("flask-webserver")
resonate = Resonate.local()
 
 
def baz(_):
    print("running baz")
    return "hello world!"
 
 
def bar(ctx):
    print("running bar")
    result = yield ctx.lfc(baz)
    return result
 
 
@resonate.register
def foo(ctx):
    print("running foo")
    result = yield ctx.lfc(bar)
    return result
 
 
@app.route("/")
def read_root():
    handle = foo.run("flask_webserver_foo_promise_id")
    return jsonify({"value": handle.result()})
 
# from example-webservers-py/flask-webserver/src/webserver.py:1-30

FastAPI swaps the constructor and the route decorator; bar / baz are structurally identical to the Flask block (imports and helpers elided):

from resonate import Resonate, Context
from resonate.stores import LocalStore
from fastapi import FastAPI
# ... bar, baz omitted; identical shape to the Flask block
 
app = FastAPI()
resonate = Resonate(store=LocalStore())
 
 
@resonate.register
def foo(ctx: Context):
    v = yield ctx.lfc(bar)
    return v + 1
 
 
@app.get("/")
def read_root():
    handle = foo.run("fastapi_webserver_foo_promise_id")
    return {"value": handle.result()}
 
# from example-webservers-py/fastapi-webserver/src/webserver.py:1-28

Django folds handle.result() into a single chained call but is otherwise the same (imports and helpers elided):

@resonate.register
def foo(ctx: Context):
    v = yield ctx.lfc(bar)
    return v + 1
 
 
def read_root(request):
    v = foo.run("django_webserver_foo_promise_id").result()
    return JsonResponse({"value": v})
 
# from example-webservers-py/django-webserver/src/django_webserver/views.py:17-25

Variants at a glance

FrameworkClient constructionHandler decoratorPromise id (hardcoded)
FlaskResonate.local()@app.route("/")flask_webserver_foo_promise_id
FastAPIResonate(store=LocalStore())@app.get("/")fastapi_webserver_foo_promise_id
DjangoResonate(store=LocalStore())function view in views.pydjango_webserver_foo_promise_id

The durable primitives in play

  • @resonate.register — registers foo as a top-level durable function under a name the SDK can route invocations to. Used at flask-webserver/src/webserver.py:20, fastapi-webserver/src/webserver.py:19, django-webserver/src/django_webserver/views.py:17.
  • Function.run(id, ...) — runs a registered function as a durable promise keyed by id. On the SDK versions the example's lockfiles pin (v0.4.12 / v0.5.1), Function.run returns Handle[T] and the caller invokes handle.result() to block (signature at resonate-sdk-py resonate/resonate.py:522-532 on v0.5.1). If a promise with the same id already exists, the call subscribes to (or returns) the existing result instead of re-executing. Called at flask-webserver/src/webserver.py:29, fastapi-webserver/src/webserver.py:27, django-webserver/src/django_webserver/views.py:24. On current main (v0.6.7) the surface changed: Function.run is now blocking and returns T directly (resonate/resonate.py:1368-1391, dedupe contract docstring at 1371-1376); Function.begin_run is the new Handle-returning variant (resonate/resonate.py:1393). See the SDK-version-drift subsection below.
  • ctx.lfc(func) — "local function call." Invokes func as a child durable step that checkpoints at invocation and at result; the parent generator yields the awaitable and the SDK resumes it with the child's value. Signature on v0.5.1 at resonate/resonate.py:305-313; on v0.6.7 at resonate/resonate.py:1020-1034. The phrase "immediate effectively-once local execution" sits in the Context.run alias docstring at resonate/resonate.py:869-873 on v0.6.7 (line 873 declares the alias). Used to chain foo → bar → baz in all three variants.
  • Resonate.local() / Resonate(store=LocalStore()) — instantiates the SDK in local mode: state lives in an in-memory store inside the Python process, with no Resonate Server required. Resonate.local() is a named classmethod (not auto-detection) at resonate/resonate.py:140-192 on v0.6.7 (and 74-92 on v0.5.1); it constructs a LocalStore internally (resonate/stores/local.py:30 on v0.6.7). The Flask variant uses the .local() helper at flask-webserver/src/webserver.py:6; FastAPI and Django pass LocalStore() explicitly (fastapi-webserver/src/webserver.py:7, django-webserver/src/django_webserver/views.py:6).

What the SDK handles vs. what you write

What you write:

  • The route handler (Flask / FastAPI / Django).
  • The registered function foo and the helpers bar, baz it composes via ctx.lfc.
  • The promise id you pass to foo.run(...).

What the SDK handles:

  • Recording a durable promise for the top-level call and for each ctx.lfc(...) step.
  • Driving the generator: when you yield ctx.lfc(bar), the SDK runs bar as a child durable step and resumes the parent generator with its result.
  • Deduplicating by promise id: a second foo.run("flask_webserver_foo_promise_id") while the first is in flight subscribes to the same promise; if the first has completed, the second returns the cached result without re-running foo.
  • Bridging the synchronous .result() call from a request thread to the SDK's worker pool.

You never write checkpoint code, never write resume-after-yield code, and never write idempotency-key plumbing — those are properties of the durable-promise model the SDK implements.

Failure modes covered

The examples run against LocalStore (in-memory), so the durability story is narrower than a remote-store deployment. What is covered:

  • Duplicate in-flight requests for the same registered function — two concurrent GET / requests hit foo.run("<id>") with the same id; the second subscribes to the first's promise rather than launching a parallel run. Backed by the dedupe contract on Function.run (docstring at resonate/resonate.py:1371-1376 on v0.6.7; on v0.5.1 Function.run delegates to Resonate.run(id, ...) at resonate/resonate.py:522-532, which is the id-keyed dedupe entrypoint).
  • Repeat requests after completion — once the first request resolves foo, every subsequent request returns the cached promise value without re-executing foo, bar, or baz. In these examples the promise id is hardcoded per sub-project (flask-webserver/src/webserver.py:29, fastapi-webserver/src/webserver.py:27, django-webserver/src/django_webserver/views.py:24), so the registered function runs at most once for the lifetime of the in-memory store. An agent porting this pattern to a real workload will want to derive the promise id from request input.

What is not covered by these examples (worth being explicit, since the integration looks more durable than it is):

  • Process-crash recovery across restarts — LocalStore is in-memory; restarting the webserver drops all promise state.
  • Cross-process work distribution — there is no separate worker; the registered function runs in the webserver's own process via the SDK's worker pool.
  • Retries, timeouts, compensation — none are configured in the example source.

SDK-version drift inside the example repo

The repo carries an internal version mismatch that an agent should resolve before running:

  • Sub-pyproject.toml files declare resonate-sdk>=0.6.7 (flask-webserver/pyproject.toml:11, fastapi-webserver/pyproject.toml:11, django-webserver/pyproject.toml:10).
  • The uv.lock files resolve to older SDKs: flask 0.5.1; fastapi and django 0.4.12 (and 0.4.10 for <3.10).
  • All three sub-READMEs say "tested with Resonate Python SDK v0.4.12" (flask-webserver/README.md:3, fastapi-webserver/README.md:3, django-webserver/README.md:3).

The handle = foo.run(...) + handle.result() pattern only compiles against <= 0.5.x. If an agent installs the pyproject's minimum (>= 0.6.7), foo.run(id) returns T (the unwrapped result), and the subsequent .result() call raises AttributeError. Run via uv sync against the lockfile to stay on the tested version, or update the call site to the v0.6.7 shape (foo.run(id) alone, or foo.begin_run(id).result()).

When to reach for this pattern

  • If you want a Python webserver route to invoke a registered function under durable execution and return its result synchronously to the HTTP caller.
  • If you want to share one function shape across Flask, FastAPI, or Django routes — the registered function code is framework-agnostic; only the route glue and the client construction call differ.
  • If you want a minimum-friction starting point to learn @resonate.register + Function.run + ctx.lfc before introducing a remote Resonate Server, multi-process workers, or longer-running registered functions.
  • If your input is small and you can derive a stable promise id from it, so dedupe behaves the way callers expect.

Do not reach for this pattern as-is if you need durability across process restarts, want durable execution to outlive the request lifecycle, or need work distributed across multiple workers — those require a Resonate Server and a RemoteStore-backed client, not the local mode used here. Also, pin the SDK against the example's lockfile (v0.4.12 / v0.5.1) before running, or update the call sites to the v0.6.7 API.

Sources