Python SDK API guidance

APIs that stay simple, even when your use cases aren't

SDK version

This page reflects resonate-sdk v0.7.0 (current on PyPI). Python ≥3.12 required. The remote-mode examples below need a running server (resonate dev, Rust server v0.9.x+); Resonate() without a url runs entirely in-process and needs no server.

Welcome to the Resonate Python SDK guide! This SDK makes it possible to write Distributed Async Await applications with Python. This guide covers installation and features that the SDK offers.

API Reference

Looking for the API reference?

The Resonate Python SDK API reference is available here.

Installation#

How to install the Resonate Python SDK into your project.

The Resonate Python SDK requires Python ≥3.12.

To install the Resonate Python SDK, you can use any of your favorite package managers.

shell
uv add resonate-sdk

Initialization#

How to initialize a Resonate Client.

There are two ways to initialize Resonate: local (in-process, no server) and remote (connected to a Resonate server).

Local initialization = zero-dependency development

Local initialization means that Resonate uses local memory for promise storage.

This is ideal for getting started quickly or for integrating Resonate into an existing application without relying on external dependencies.

python
from resonate.resonate import Resonate

resonate = Resonate()
Incremental adoption

Temporal, Restate, and DBOS require a server or database to get started.

Resonate enables you to get started with a local worker that stores promises in memory, no server or database required. This makes it easy to incrementally adopt Resonate into your existing Python application.

Remote initialization

Remote initialization connects to a running Resonate server, enabling durable promise storage and cross-process RPC. Start a server locally with resonate dev, then connect:

python
from resonate.resonate import Resonate

resonate = Resonate(url="http://localhost:8001")

The url parameter takes precedence over the RESONATE_URL environment variable, which in turn takes precedence over the RESONATE_HOST/RESONATE_PORT fallback. If none of these are set, the client runs in local (in-process) mode.

To pass an auth token:

python
from resonate.resonate import Resonate

resonate = Resonate(url="http://localhost:8001", token="your-token")

The token kwarg falls back to the RESONATE_TOKEN environment variable when omitted.

Concurrency#

A single Python worker process executes multiple functions concurrently inside a single asyncio event loop. The v0.7.0 SDK is fully async; every durable function is an async def coroutine and effects are awaited.

See Scaling — Worker concurrency for more on concurrency strategies.

Client APIs#

Registration

How to register a function with Resonate in the Python SDK.

There are two ways to register a function with Resonate: using the register() method or using the @resonate.register decorator.

@resonate.register#

Decorator

python
@resonate.register
async def foo(ctx, arg):
    # ...
    return result

.register()#

Method

python
async def foo(ctx, arg):
    # ...
    return result

resonate.register(foo)

.with_dependency()#

Resonate's .with_dependency() method allows you to store a typed dependency on the client. You can then access the dependency in any durable function using the .get_dependency() method.

Dependencies can only be added in the ephemeral world, before the system starts processing tasks.

python
resonate.with_dependency(dependency)

The dependency is keyed by its concrete type and is accessible from any function in the call graph on that Application Node. This is useful for things like database connections or other resources that you want to share across functions.

How to invoke a function in the ephemeral world with the Resonate Class.

To move from the ephemeral world to the durable world you use the Resonate Class to invoke functions.

There are two methods that you can use: .run() and .rpc().

.run()#

Resonate's .run() method invokes a function in the same process and returns a handle synchronously. You can think of it as a "run right here" invocation. After invocation, the function is considered durable and will recover in another process if required.

python
import asyncio
from resonate.resonate import Resonate

async def main():
    resonate = Resonate(url="http://localhost:8001")
    resonate.register(foo)
    handle = resonate.run("invocation-id", foo, arg)
    result = await handle.result()
    await resonate.stop()

asyncio.run(main())

.rpc()#

Resonate's .rpc() method (Remote Procedure Call) invokes a function in a remote process and returns a handle synchronously. You can think of it as a "run somewhere else" invocation (Asynchronous Remote Procedure Call). After invocation, the function is considered durable and will recover in another process if required.

python
import asyncio
from resonate.resonate import Resonate

async def main():
    resonate = Resonate(url="http://localhost:8001")
    handle = resonate.rpc("invocation-id", "foo", arg)
    result = await handle.result()

    # optionally target a specific group
    handle2 = resonate.options(target="workers").rpc("invocation-id-2", "foo", arg)
    result2 = await handle2.result()
    await resonate.stop()

asyncio.run(main())

.options()#

Options can be used preceding .run() and .rpc() on the client.

python
from datetime import timedelta
from resonate.retry import Exponential, Constant, Linear, Never

resonate.options(
    timeout=timedelta(seconds=60),
    target="workers",
    version=1,
).run("invocation-id", foo, arg)

.get()#

Resonate's .get() method allows you to subscribe to a function invocation by its promise ID. If the invocation does not exist, an error will be thrown. .get() is async.

python
handle = await resonate.get("invocation-id")
result = await handle.result()

.promises.get()#

Resonate's .promises.get() method allows you to get a promise by ID.

python
await resonate.promises.get("promise-id")

.promises.create()#

Resonate's .promises.create() method allows you to create a promise.

python
import time
from resonate.types import Value

await resonate.promises.create(
    id="promise-id",
    timeout_at=int(time.time() * 1000) + 30000,  # 30s in the future, in milliseconds
    param=Value(data=None),
    tags={},
)

.promises.resolve()#

Resonate's .promises.resolve() method allows you to resolve a promise by ID.

This is useful for HITL use cases where you want to unblock a durable function waiting on a human action. It works well in conjunction with the .promise() method.

python
from resonate.types import Value

await resonate.promises.resolve(
    id="promise-id",
    value=Value(data={"approved": True}),
)

.promises.reject()#

Resonate's .promises.reject() method allows you to reject a promise by ID.

python
from resonate.types import Value

await resonate.promises.reject(
    id="promise-id",
    value=Value(data={"reason": "policy violation"}),
)

.stop()#

.stop() performs a graceful async shutdown — it tears down the transport, heartbeat, and runtime tasks. Call it from any process that should exit cleanly after its work finishes.

main.py·python
import asyncio
from resonate.resonate import Resonate

async def main():
    resonate = Resonate(url="http://localhost:8001")
    resonate.register(greet)

    handle = resonate.run("greet-1", greet, "world")
    result = await handle.result()
    print(result)

    await resonate.stop()

asyncio.run(main())
Don't call `.stop()` on a long-running worker

Calling .stop() on a worker (an RPC service, a Kafka consumer, an MCP server, or any other long-lived process) tears down the channels the worker uses to receive and hold work:

  • The transport connection stops — the worker stops receiving dispatched tasks.
  • The heartbeat stops — the server-side TTL on in-flight tasks expires, and the server reassigns them.

The process keeps running but silently stops processing work. Workers should stay up; let process termination (SIGINT / SIGTERM) end the lifecycle.

After calling .stop(), the client should not be used for further operations — construct a new Resonate instance instead.

Context APIs#

How to use the Resonate Context object in the Python SDK.

Resonate's Context object enables you to invoke functions from inside a Durable Function. This is how you extend the Call Graph and create a world of Durable Functions. Inside a Durable Function you use the await keyword to interact with the Context object.

.get_dependency()#

Context's .get_dependency() method allows you to get a typed dependency that was registered in the ephemeral world using .with_dependency() and use it in the Durable World.

python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    dependency = ctx.get_dependency(MyDependencyType)
    # do something with the dependency

.run()#

Context's .run() method invokes a function in the same process.

Await it immediately to block until the child resolves:

python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    result = await ctx.run(bar, arg)
    # do more stuff


async def bar(ctx: Context, arg):
    return

Invoke without awaiting to dispatch multiple children concurrently, then await them later:

python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    f1 = ctx.run(bar, arg)
    f2 = ctx.run(bar, arg)
    result1 = await f1
    result2 = await f2


async def bar(ctx: Context, arg):
    return

.rpc()#

The RPC API is how you durably communicate between processes. It is how you invoke functions in other processes and extend the Call Graph across process boundaries.

Await it immediately (synchronous-style):

python
from resonate.context import Context

# process x
@resonate.register
async def foo(ctx: Context, arg: str) -> str:
    result = await ctx.rpc("bar", arg)
    # do more stuff


# process y
@resonate.register
async def bar(ctx: Context, arg: str) -> str:
    return

Invoke without awaiting to dispatch multiple RPCs concurrently, then await them later:

python
from resonate.context import Context

# process a
@resonate.register
async def foo(ctx: Context, arg):
    f1 = ctx.rpc("bar", arg)
    f2 = ctx.rpc("bar", arg)
    result1 = await f1
    result2 = await f2

# process b
@resonate.register
async def bar(ctx: Context, arg):
    return

.detached()#

Context's .detached() method invokes a function in a remote process in a fire-and-forget manner. Unlike holding an RPC future, the detached child's lifetime is fully decoupled from the parent — the parent never implicitly awaits it, and the child survives the parent completing.

python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    audit_future = ctx.detached("bar", arg)
    audit_id = await audit_future.id()
    # foo completes; bar keeps running independently

.options()#

Many of Context's methods support per-call options. Call .options(...) on the context before .run(), .rpc(), or .detached().

Both local calls (.run()) and remote calls (.rpc() / .detached()) accept timeout, target, version, and retry_policy.

python
from datetime import timedelta
from resonate.retry import Exponential, Constant, Linear, Never
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    result = await ctx.options(
        retry_policy=Exponential(delay=1, factor=2, max_delay=30, max_retries=10),
        timeout=timedelta(seconds=30),
    ).run(bar, arg)
python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    result = await ctx.options(
        target="workers",
        version=1,
    ).rpc("bar", arg)

.promise()#

Context's .promise() method creates a durable promise that can be awaited by the calling function and resolved externally.

This is very useful for HITL (Human-In-The-Loop) use cases where you want to block progress until a human has taken an action or provided data. It works well in conjunction with the .promises.resolve() method.

python
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    approval = ctx.promise()          # returns a future; inherits workflow timeout
    approval_id = await approval.id() # get the externally-addressable promise ID
    # publish approval_id to a reviewer (email, Slack, dashboard, etc.)
    decision = await approval         # suspend until the external party resolves it

.sleep()#

Context's .sleep() method suspends the function durably for the given duration. There is no limit to how long you can sleep. The method accepts a timedelta from the standard library.

python
from datetime import timedelta
from resonate.context import Context

@resonate.register
async def foo(ctx: Context, arg):
    await ctx.sleep(timedelta(seconds=5))  # sleep for 5 seconds
    # do more stuff

Deterministic time and random values#

ctx.time and ctx.random from v0.6.x have been removed. There is no replacement helper.

The v0.7.0 idiom for nondeterministic values that must be stable across replay is to call the nondeterministic operation inside a leaf function invoked via await ctx.run(leaf, ...). The leaf's return value is durably checkpointed, so replay yields the same value without re-running the leaf.

python
import time
import random
from resonate.context import Context

async def get_timestamp(ctx: Context) -> float:
    return time.time()

async def get_random(ctx: Context) -> float:
    return random.random()

@resonate.register
async def foo(ctx: Context, arg):
    ts = await ctx.run(get_timestamp)   # checkpointed; same value on replay
    rand = await ctx.run(get_random)    # checkpointed; same value on replay
    # use ts and rand safely

Defaults#

The Python SDK ships with these key defaults.

  • Resonate()group = "default", log_level = logging.INFO. URL falls back to RESONATE_URL, then RESONATE_HOST/RESONATE_PORT; otherwise local mode.
  • ctx.run / Optionstimeout = timedelta(days=1), target = "default", version = 1.
  • retry_policy for ctx.run — resolves to Exponential(delay=1, factor=2, max_delay=2**63 - 1, max_retries=30).
  • Exponential() required fieldsdelay, factor, max_delay, max_retries (all times in seconds).
  • Constant() / Linear() required fieldsdelay, max_retries.
Python times are seconds, TypeScript times are milliseconds

The Python SDK uses seconds for timeout and retry-policy delays. The TypeScript SDK uses milliseconds. The numbers do not transfer directly between the two.

For the full table, per-SDK comparison, and source citations, see the Defaults reference.