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-30FastAPI 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-28Django 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-25Variants at a glance
| Framework | Client construction | Handler decorator | Promise id (hardcoded) |
|---|---|---|---|
| Flask | Resonate.local() | @app.route("/") | flask_webserver_foo_promise_id |
| FastAPI | Resonate(store=LocalStore()) | @app.get("/") | fastapi_webserver_foo_promise_id |
| Django | Resonate(store=LocalStore()) | function view in views.py | django_webserver_foo_promise_id |
The durable primitives in play
@resonate.register— registersfooas a top-level durable function under a name the SDK can route invocations to. Used atflask-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 byid. On the SDK versions the example's lockfiles pin (v0.4.12/v0.5.1),Function.runreturnsHandle[T]and the caller invokeshandle.result()to block (signature atresonate-sdk-pyresonate/resonate.py:522-532onv0.5.1). If a promise with the sameidalready exists, the call subscribes to (or returns) the existing result instead of re-executing. Called atflask-webserver/src/webserver.py:29,fastapi-webserver/src/webserver.py:27,django-webserver/src/django_webserver/views.py:24. On currentmain(v0.6.7) the surface changed:Function.runis now blocking and returnsTdirectly (resonate/resonate.py:1368-1391, dedupe contract docstring at1371-1376);Function.begin_runis the newHandle-returning variant (resonate/resonate.py:1393). See the SDK-version-drift subsection below.ctx.lfc(func)— "local function call." Invokesfuncas 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 onv0.5.1atresonate/resonate.py:305-313; onv0.6.7atresonate/resonate.py:1020-1034. The phrase "immediate effectively-once local execution" sits in theContext.runalias docstring atresonate/resonate.py:869-873onv0.6.7(line 873 declares the alias). Used to chainfoo → bar → bazin 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) atresonate/resonate.py:140-192onv0.6.7(and74-92onv0.5.1); it constructs aLocalStoreinternally (resonate/stores/local.py:30onv0.6.7). The Flask variant uses the.local()helper atflask-webserver/src/webserver.py:6; FastAPI and Django passLocalStore()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
fooand the helpersbar,bazit composes viactx.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 runsbaras 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-runningfoo. - 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 hitfoo.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 onFunction.run(docstring atresonate/resonate.py:1371-1376onv0.6.7; onv0.5.1Function.rundelegates toResonate.run(id, ...)atresonate/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-executingfoo,bar, orbaz. 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 —
LocalStoreis 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.tomlfiles declareresonate-sdk>=0.6.7(flask-webserver/pyproject.toml:11,fastapi-webserver/pyproject.toml:11,django-webserver/pyproject.toml:10). - The
uv.lockfiles resolve to older SDKs: flask0.5.1; fastapi and django0.4.12(and0.4.10for<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.lfcbefore 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
- Example repo: https://github.com/resonatehq-examples/example-webservers-py
- Resonate Python SDK: https://github.com/resonatehq/resonate-sdk-py
Context.lfcatv0.6.7: https://github.com/resonatehq/resonate-sdk-py/blob/v0.6.7/resonate/resonate.py#L1020-L1034Function.runatv0.6.7(returnsT): https://github.com/resonatehq/resonate-sdk-py/blob/v0.6.7/resonate/resonate.py#L1368-L1391Function.runatv0.5.1(returnsHandle[T], matches the example): https://github.com/resonatehq/resonate-sdk-py/blob/v0.5.1/resonate/resonate.py#L522-L532Resonate.local()classmethod (v0.6.7): https://github.com/resonatehq/resonate-sdk-py/blob/v0.6.7/resonate/resonate.py#L140-L192LocalStoreclass (v0.6.7): https://github.com/resonatehq/resonate-sdk-py/blob/v0.6.7/resonate/stores/local.py#L30- Resonate docs: https://docs.resonatehq.io
