| name | resonate-basic-debugging-python |
| description | Debug and troubleshoot Resonate applications using the Python SDK v0.7.0 and Resonate Server v0.9.x. Use when investigating stuck workflows, non-deterministic replays, registration errors, async/await mistakes, unexpected retries, or server connectivity issues. |
| license | Apache-2.0 |
Resonate Debug and Troubleshoot — Python
Overview
Use this skill to diagnose Resonate runtime issues in Python. The Python SDK v0.7.0 uses async def + await (not generators); failure modes differ from TypeScript's generator-based v1 API and from the legacy v0.6.x Python SDK. Focus on reproducible symptoms, promise state, registration, and determinism.
For the language-agnostic replay + recovery mental model, read durable-execution first.
Triage flow
- Classify the failure: import/syntax error, runtime SDK error, server error code, workflow stuck/hanging, or unexpected replay behavior.
- Confirm Python is ≥ 3.12 and
resonate-sdk>=0.7.0 is installed.
- Confirm the Resonate server is reachable:
resonate dev starts it on localhost:8001 (Rust v0.9.x, single port — no separate store port).
- Confirm the worker process is running, connected to the right URL and group, and registered for the right functions.
- Inspect the promise state and call graph for the invocation ID.
- Validate determinism — no
time.time(), no random.random(), no un-checkpointed side effects in the orchestrator body.
Version + compat pins
- Python ≥ 3.12 — the SDK uses recent typing syntax. 3.11 and below won't import.
resonate-sdk>=0.7.0 — async/await rewrite; incompatible with v0.6.x API.
- Resonate server v0.9.x (Rust, single port 8001) — run with
resonate dev for local development.
resonate-sdk on PyPI (not resonate — the bare name is a different package).
If from resonate.resonate import Resonate fails, you either have the wrong package installed or you're on Python < 3.12.
Async/await pitfalls (Python-specific)
These are the most common Python-only footguns with the v0.7.0 SDK.
Forgot await before a Context call
async def foo(ctx: Context, arg: str) -> str:
ctx.run(bar, arg)
return "done"
The future is tracked by structured concurrency (the runtime will still join it before settling foo), but the result is lost and your orchestrator continues without it. Fix:
result = await ctx.run(bar, arg)
Used yield instead of await
async def foo(ctx: Context, arg: str) -> str:
result = yield ctx.run(bar, arg)
The v0.7.0 SDK uses async def + await, not def + yield. Mixing them produces a TypeError or unexpected behavior. Use await.
Made the function a plain def instead of async def
def foo(ctx: Context, arg: str) -> str:
result = await ctx.run(bar, arg)
All durable functions must be async def. The SDK registers and dispatches them as coroutines.
Not awaiting r.promises.*
r.promises.resolve(approval_id, Value(data={"ok": True}))
All r.promises.* methods are async. Missing await means the resolve is never sent. Fix:
await r.promises.resolve(approval_id, Value(data={"ok": True}))
Not calling await r.stop() on shutdown
The SDK needs to drain in-flight work cleanly. Always:
try:
result = await r.run(id, fn, arg).result()
finally:
await r.stop()
Registration errors
| Symptom | Likely cause | Fix |
|---|
Function 'X' is not registered | rpc target name doesn't match a registered name on any reachable worker | Check r.register(fn) is called; ensure the calling name and registered name match (if you used r.register(fn, name="custom") the caller must use "custom") |
Function version must be greater than zero | Passing version=0 explicitly | Default is 1; omit version or pass a positive integer |
Args unencodeable | Argument to run/rpc not JSON-serializable | Use dicts, lists, strings, numbers, bools, None, or types with custom encoders registered |
Promise-state errors
| Code | Meaning | Typical cause |
|---|
| 40900 | Promise already exists | Two calls used the same invocation ID; treat as idempotency — the second call returns the first's result. If you wanted a fresh run, change the ID |
| 40400 | Promise not found | Wrong ID, wrong server URL, or promise was purged |
| 40300 | Already resolved | Normal if you're checking idempotency; the value is the first resolution |
| 40301 | Already rejected | Normal; the reason is the first rejection |
| 40303 | Already timed out | Promise's deadline passed; re-create with a new ID if you want to retry |
Inspect from the CLI:
resonate promises get <promise-id>
resonate tree <invocation-id>
resonate promises search <id-prefix>
"My workflow hangs"
Three common causes, in order of likelihood:
- No worker for the target group. If your
rpc targets "backend" but no process has registered with group="backend", the invocation is queued forever. Check: resonate promises get <id> → state stays pending; confirm your worker process is running and connected.
- Forgot
await (see pitfalls above). The orchestrator advanced past a step without waiting for it.
- Server not reachable. Confirm
resonate dev is running on port 8001; check RESONATE_URL env var.
"My workflow replays things it shouldn't"
Replays are normal — Resonate re-executes the durable function from the top on every resumption, using stored promise values for the awaited checkpoints. Side effects outside the ctx.run envelope re-execute on every replay.
async def foo(ctx: Context, arg: str) -> str:
print(f"starting {arg}")
log.info(f"starting {arg}")
result = await ctx.run(work, arg)
return result
Fix: move any side effect into a helper called via ctx.run:
async def log_start(ctx: Context, arg: str) -> None:
log.info(f"starting {arg}")
async def foo(ctx: Context, arg: str) -> str:
await ctx.run(log_start, arg)
result = await ctx.run(work, arg)
return result
Non-determinism regressions
Common non-deterministic code that breaks recovery:
async def bad(ctx: Context, x: str) -> str:
t = time.time()
r = random.random()
path = os.environ.get("FEATURE")
if t > 1700000000 or r > 0.5:
return await ctx.run(branch_a)
return await ctx.run(branch_b)
Each resumption may hit a different branch than the original run. Wrap non-deterministic values in leaf functions via ctx.run:
async def _get_time(ctx: Context) -> float:
return time.time()
async def _get_random(ctx: Context) -> float:
return random.random()
async def good(ctx: Context, x: str) -> str:
t = await ctx.run(_get_time)
r = await ctx.run(_get_random)
Minimal repro templates
Worker:
from __future__ import annotations
import asyncio, os
from typing import TYPE_CHECKING
from resonate.resonate import Resonate
if TYPE_CHECKING:
from resonate.context import Context
r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"), group="workers")
async def ping(ctx: Context, name: str) -> str:
return f"pong {name}"
r.register(ping)
async def main() -> None:
try:
await asyncio.Event().wait()
finally:
await r.stop()
asyncio.run(main())
Client:
from __future__ import annotations
import asyncio, os, time
from resonate.resonate import Resonate
async def main() -> None:
r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"))
try:
result = await r.options(target="workers").rpc(
f"ping-{time.time_ns()}", "ping", "alice"
).result()
print(result)
finally:
await r.stop()
asyncio.run(main())
If the client hangs, check: worker running? same URL? worker in the right group? resonate promises get <id> → what state is it in?
CLI one-liners
# local dev server — zero config, single port 8001
resonate dev
# inspect a workflow's tree of promises
resonate tree <invocation-id>
# get a specific promise's state
resonate promises get <id>
# resolve/reject external promises from the CLI (e.g., for HITL testing)
resonate promises resolve <id> --data '{"approve": true}'
resonate promises reject <id> --data '{"reason": "denied"}'
# search by prefix or tags
resonate promises search 'order:*'
Related skills
resonate-basic-ephemeral-world-usage-python — Client APIs; useful when the failure is at the process-entry layer
resonate-basic-durable-world-usage-python — Context APIs; useful when the failure is inside a durable function
durable-execution — foundational; the mental model for recovery + replay
resonate-philosophy — the "do not build these things" list; many debug sessions end up being about patterns that were already warned against here