Skip to main content

Develop with Resonate

The Resonate API surface area is designed to fit the needs of a distributed application developer.

The API is intentionally minimal. With just a small set of primitives, you can do most of the heavy lifting required for building distributed applications. This surface area was designed carefully: it’s simple to learn, yet powerful enough to address the core challenges developers face when writing resilient, distributed systems.

Every API call in Resonate is durable by default. That means your operations survive process crashes, restarts, and unexpected failures. You don’t have to wire up custom retry loops or invent your own fault tolerance patterns — Resonate takes care of this, letting you focus on your business logic instead of infrastructure plumbing.

Distributed applications introduce unique problems: coordination across processes, ordering of events, fault handling, and more. Developers often patch these problems with ad-hoc solutions that make the business process hard to follow and even harder for new contributors to understand. Resonate smooths away this complexity by giving you APIs that make sense at the business-logic level, while still solving for the hard distributed systems problems underneath.

These docs aim to meet you where you are — whether you’re new to distributed programming or already familiar with the common pain points — while guiding you into the “Resonate way” of thinking about system design. Each code sample shown here is deliberately minimal, to highlight exactly what the API looks like and where it fits in the broader ecosystem of distributed application development. From there, you’ll find links to more detailed examples with step-by-step explanations.

Installing SDKs

Resonate SDKs can be installed using your favorite package manager.

Quick example:

uv add resonate-sdk

More examples.

Initializing clients

You need to initialize a Resonate Client in each process that you want to use Resonate.

A Resonate Client connects to a Resonate Server and message sources while providing the top level APIs needed to durably invoke function executions.

It is important to note that your Resonate Client does not need to connect to a Resonate Server to start developing. You can use the Resonate Client in "local mode", working with functions that are local to the process.

This is in contrast to Temporal, Restate, and DBOS which require connecting to a server or database, even for development.

Resonate calls this capability "Zero dependency development".

Quick example:

from resonate import Resonate

# Initialize a Resonate Client in local mode
resonate = Resonate.local()

More examples.

Client APIs

Many Resonate Client APIs can be used just about anywhere. Seriously. For example, the Resonate TypeScript SDK can run in your browser, enabling Async RPCs from your frontend code, or converting your browser session into a worker.

Registering functions

Registering a function is a lot like exposing a service endpoint.

Resonate promotes a concept called the Call Graph. Effectively, it is a graph of function calls that can span multiple processes. The first function in the Call Graph is called the "top level" function.

The "top level" function must be registered with Resonate. A registered function becomes a Durable Function. A registered function can be invoked from any other process with a Resonate Client connected to the same Resonate Server.

The first argument of a registered function will always be a Resonate Context, which can be used to call other functions. Local functions do not need to be registered, but will become Durable Functions if invoked from the registered Durable Function.

Note that all parameters passed to a Durable Functions must be serializable.

Quick example:

In Python, you can use the @resonate.register decorator to register a function:

@resonate.register
def foo(context: Context, arg: str) -> str:
# ...
return result

More examples.

Run

Run is the workhorse API for your local process.

The Run API on the Resonate Client is for invoking functions locally (in the same process). Basically, its the equivalent of a normal function call, but it creates a checkpoint so even if your process crashes, the caller function can resume in a new process.

The Run API is synchronous, meaning it will block until the callee function completes and returns a result. So there is also a Begin Run API, which is asynchronous and does not block.

Basically Run returns the value of the function being called while Begin Run returns a promise/handle to the function being called, which can be used to get the result later.

Quick example:

# process x
# foo() and main() are in the same process
@resonate.register
def foo(ctx: Context, arg: str) -> str:
# ...
return

def main():
# synchronous call with Run API
result = foo.run("invocation-id", "Hello World!")

# asynchronous call with Begin Run API
handle = foo.begin_run("invocation-id", "Hello World!")
# ...
result = handle.result()

More examples.

RPC

If the Run API is the workhorse of your local process, then the RPC API is a magical unicorn for your distributed application.

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

RPC is synchronous, and blocks the calling function until the invoked function returns. Begin RPC is asynchronous, and returns a promise which can be awaited later.

Quick example:

# foo is in process x
@resonate.register
def foo(context: Context, arg: str):
# ...
return result


# main is in process y
def main():
# synchronously invoke foo
result = resonate.options(target="process-group-x").rpc(
"invocation_id", func="foo", arg="Hello World!"
)

# asynchronously invoke foo
handle = resonate.options(target="process-group-x").begin_rpc(
"invocation_id", func="foo", arg="Hello World!"
)
# do more stuff
result = handle.result()

More examples.

Promises

Promises are the key primitive for asynchronous programming in Resonate. They allow you to represent a value that may not be available yet, but will be at some point in the future. As a developer, you are effectively programming with async/await across processes. Resonate promises are durable, however, persisted by the Resonate Server.

Each promise is unique, identified by an ID. The promise ID is a "write-once" value, meaning it can only be set once and cannot be changed. It can be used as an idempotency_key, ensuring that multiple attempts to take a step will not result in duplication.

The Resonate Client enables you to create, get, resolve, and reject promises.

These APIs are make it possible to manage the lifecycle of a promise outside of the Call Graph.

The Create Promise API is useful for creating a promise before invoking the function that will resolve it later. And a common reason for using the Get Promise API, is to check whether a promise exists, and if it does, whether it is resolved. For example, if you have a long-running background job that you want to check on, you can use the get promise API to get the handle of the promise and check if it is resolved yet.

Quick examples:

# create a promise
resonate.promises.create(
id="promise-id",
timeout=int(time.time() * 1000) + 30000, # 30s in the future
)
# RE: Promise creation resolution timeout
# Functions await on other functions through Durable Promises.
# In Resonate, a timeout is associated with the Durable Promise resolution, not individual function executions.
# Resonate attempts to resolve a Durable Promise until the specified timeout is reached.
# If the timeout is reached, Resonate marks the Durable Promise as failed.

# get a promise
resonate.promises.get("promise-id")

# resolve a promise
resonate.promises.resolve("promise-id")

# reject a promise
resonate.promises.reject("promise-id")

More examples.

Naming scheme best practices

When designing the naming scheme for your durable promise IDs, keep the following considerations in mind:

  1. Uniqueness: The naming scheme should guarantee uniqueness to avoid conflicts between executions. If an ID is reused for a different execution, it will result in retrieving the stored result of the previous execution instead of starting a new one.
  2. Readability: Choose a naming scheme that is easy to understand and interpret, making it easier to debug and manage executions.
  3. Relevance: Incorporate relevant information into the naming scheme, such as the purpose or context of the execution.

There are several approaches to ensure your Durable Promise IDs are unique but also readable and relevant.

Date-based

One very common approach is to use the date as part of the naming scheme to. For example, if you have a durable promise that fetches and aggregates news articles on a daily basis, you could include the date in the ID format to ensure uniqueness and provide clear indication of when the execution occurred.

news_feed_YYYY-MM-DD

Hierarchical

You can use a hierarchical naming scheme similar to file system paths to represent the identity of a durable promise. The naming scheme can include information such as the environment, service, and specific execution details. For example:

staging/analytics/monthly-report/2023-05

Platform-specific

If your durable promises are running on a specific platform or orchestrator, you can incorporate the platform's identity concepts into the naming scheme. For example, if you are using Kubernetes, you can include the namespace, pod, and other relevant information:

k8s/staging/namespace/analytics/gpu/h100/monthly-report-2023-05

Opaque with metadata

In this case, the durable promise ID is a randomly generated unique identifier, and you would store the associated metadata (such as environment, service, execution details) in a separate database that can be queried using the ID.

executions/a7b89c3d-f012-4e78-9a7d-89a3f6b2e1c7

Dependency injection

Let's say that you have initialized a DB connection or some other resource that some of your functions in that process might need to use. You can set a dependency with the Resonate Client which makes it available to all Durable Functions in that process.

Quick example:

resonate.set_dependency("db", db_connection)

More examples.

Function APIs

These are durable to durable APIs that are used inside the "durable world" — a world that automatically resumes after recovery.

Run

The Run API is also available from the Context object, the first argument of any Durable Function. The difference between the Run API on Context vs the Client, is that the Context Run API is used from within Durable Functions. The Client Run API is used in regular "ephemeral" functions.

Again, Run is synchronous and Begin Run is asynchronous.

Use the Run API when you want to checkpoint the next step in your application / workflow. This checkpoint makes it possible to replay the calling function with the result of the callee.

@resonate.register
def foo(ctx: Context, arg: string):

# synchronously call bar
result = yield ctx.run(bar, arg)

# asynchronously call bar
promise = yield ctx.run(bar, arg)
# do more stuff
result = promise.result()

def bar(ctx: Context, arg: string):
# ...
return result

More examples.

RPC

The RPC API is also available from the Context object, the first argument of any Durable Function. The difference between the RPC API on Context vs the Client, is that the Context RPC API is used from within Durable Functions. The Client RPC API is used in regular "ephemeral" functions.

Again, RPC is synchronous and Begin RPC is asynchronous.

Use the RPC API when you want to call a function in another process and checkpoint it in your application / workflow. This checkpoint makes it possible to replay the calling function with the result of the callee.

Quick example:

# process x
@resonate.register
def foo(ctx: Context, arg: str) -> str:
# synchronous invocation of bar
result = yield ctx.rpc("bar", arg)

# asynchronous invocation of bar
promise = yield ctx.begin_rpc("bar", arg)
# do more stuff
result = yield promise


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

More examples.

Get dependency

Get a dependency that was set on the Resonate Client in the same process.

@resonate.register
def foo(ctx, arg):
# ...
db_conn = ctx.get_dependency("database-connection") # get database connection
# use database connection
# ...

More examples.

Get or create promise

@resonate.register
def foo(ctx, arg):
# ...
promise = yield ctx.promise(id="promise-id") # optionally supply an id
# do something
yield promise # block on the promise's resolution
# do more stuff
# ...

More examples.

Sleep

Context's sleep method is durable across replays. That means that, once invoked, a replay of the function will not reset, extend, or retrigger the sleep duration.

Additionally, this sleep method is non-blocking, meaning that other functions in the process will be able to make progress even if some are sleeping.

@resonate.register
def foo(ctx, arg):
# ...
yield ctx.sleep(5.0) # sleep for 5 seconds
# ...

More examples.

Generate random number

Use Context's random methods to generate random numbers to ensure a deterministic number across replays.

Quick example:

@resonate.register
def foo(ctx, arg):
# ...
rand = yield ctx.random.random() # ensures the result is the same across replays
# do something with rand
# ...

More examples.

Get the time

Use Context's time methods to get the current time to ensure a deterministic time across replays.

Quick example:

@resonate.register
def foo(ctx, arg):
# ...
time = yield ctx.time.time() # ensures the result is the same across replays
# do something with time
# ...

More examples.