temporalio review
temporalio is Temporal's Python SDK for business processes that must resume after worker crashes, deployments, or long waits. A Temporal service stores event history; workers replay deterministic workflow functions and run side effects in retryable activities. Version 1.32.0 adds deterministic `workflow.uuid7()`, replay-safe OpenTelemetry meter and logger wrappers, configurable Core log formats, payload-validation errors, and a bounded Pydantic type-adapter cache. It also changes experimental OpenAI Agents and Nexus behavior. The SDK alone is not a queue: it needs a Temporal Cloud or self-hosted service plus continuously polling workers.
temporalio 1.31.0 installed in 0.7 seconds and used 56 MB across 5 packages with 0 audit findings in our sandbox; PyPI has since moved to 1.32.0. Adopt it when business state must survive failures and long waits, and avoid the service plus replay model for ordinary queued jobs.
We installed it
| Install | ✓ · 0.7s | 5 packages on disk · 56 MB |
| Import | ✓ | import temporalio in 0.45s · compiled extensions · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does temporalio install cleanly?
Yes. In a fresh container with an empty cache, pip install temporalio finished in 0.7s, leaving 5 packages and 56 MB on disk. pip-audit reported no known vulnerabilities.
What does temporalio need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import temporalio succeeded in 0.45s, and the package ships py.typed for type checkers.
temporalio or celery: which should you use?
celery: Choose it for broker-backed jobs when durable event-history replay and workflow signals are unnecessary. temporalio 1.31.0 installed in 0.7 seconds and used 56 MB across 5 packages with 0 audit findings in our sandbox; PyPI has since moved to 1.32.0.
When should you not use temporalio?
You only need fire-and-forget background tasks. Celery has fewer concepts and does not require workflow history replay.
Use it if
- A payment, fulfillment, approval, or onboarding process must preserve progress across outages and deploys.
- The design needs durable timers, signals, queries, updates, child workflows, retries, or compensation with recorded history.
- The team can operate a Temporal namespace, task queues, workers, workflow IDs, and deployment compatibility.
- External I/O can live in idempotent activities while workflow code stays deterministic.
- You only need fire-and-forget background tasks. Celery has fewer concepts and does not require workflow history replay.
- A new control-plane service is unacceptable. This package cannot execute durable work without reachable Temporal servers and matching workers.
- The orchestration function must freely read files, environment variables, wall-clock time, or network state. Replay forbids nondeterministic side effects in workflow code.
- Activities cannot tolerate duplicate execution. Temporal may retry after an external side effect succeeded but before its completion was recorded.
- Production workflow code changes without replay tests. An innocent branch or command-order change can become nondeterministic against stored histories.
Setup reality
We installed temporalio 1.31.0 in a fresh Python 3.12 Bookworm sandbox. The install took 0.7 seconds, produced 5 packages, and occupied 56 MB. That release declared 23 direct dependencies, required Python 3.10 or newer, and shipped compiled .so files plus py.typed. import temporalio succeeded in 0.45 seconds, and pip-audit found 0 known vulnerabilities. The license was MIT. PyPI now serves 1.32.0, so none of those measured sizes or timings describe the newer wheel.
A client needs a service address and namespace. Production commonly adds TLS plus an API key or mTLS certificate. A worker must reach that service and poll the exact task_queue used when a workflow starts. Choose workflow IDs around business uniqueness to prevent duplicate starts. Arguments and results become history payloads, so keep credentials and large blobs out unless a reviewed codec or external payload store protects them.
Workflow functions can replay many times. Use workflow.now(), workflow.random(), and workflow.uuid4() or the new 1.32.0 workflow.uuid7() instead of ordinary time and randomness. HTTP, SQL, file access, and third-party SDK calls belong in activities. Replay captured production histories before deployment, and use versioning APIs for command changes. The sandbox helps detect determinism mistakes; it is not a security boundary.
An async activity must not block the event loop. Synchronous activities need an executor sized for worker concurrency. Set an activity timeout, choose retries around duplicate-safe effects, and heartbeat long jobs so cancellation and progress work. Worker slots, pollers, sticky workflow cache, shutdown grace, and actual I/O latency determine capacity. Version 1.32.0 also warns when a workflow task exceeds 5 seconds by default, which is a signal to move blocking or CPU work out of the workflow.
Patterns
Put payment I/O in an activity define-idempotent-activity
from temporalio import activity
@activity.defn
async def charge_order(order_id: str) -> str:
return await payments.charge(
order_id,
idempotency_key=order_id,
)An activity can execute more than once, so the payment API needs a stable idempotency key.
Run an activity from a workflow define-workflow
from datetime import timedelta
from temporalio import workflow
with workflow.unsafe.imports_passed_through():
from activities import charge_order
@workflow.defn
class Checkout:
@workflow.run
async def run(self, order_id: str) -> str:
return await workflow.execute_activity(
charge_order,
order_id,
start_to_close_timeout=timedelta(minutes=2),
)Workflow code must replay deterministically; the activity contains the external side effect.
Connect to a secured namespace connect-secure-client
from temporalio.client import Client
client = await Client.connect(
'cluster.example.com:7233',
namespace='payments',
tls=True,
api_key=temporal_api_key,
)Keep the API key in worker or client configuration, never in workflow arguments that become history.
Poll the payment task queue run-task-queue-worker
from temporalio.worker import Worker
worker = Worker(
client,
task_queue='payments',
workflows=[Checkout],
activities=[charge_order],
)
await worker.run()A worker can only execute types it registers, and its task queue must match the workflow start request.
Use a business-specific workflow ID start-unique-workflow
handle = await client.start_workflow(
Checkout.run,
'order-123',
id='checkout-order-123',
task_queue='payments',
)
result = await handle.result()Workflow ID reuse and conflict policies decide what a duplicate business request does; choose them deliberately.
Wait for an approval signal receive-workflow-signal
@workflow.defn
class Approval:
def __init__(self) -> None:
self.approved = False
@workflow.run
async def run(self) -> None:
await workflow.wait_condition(lambda: self.approved)
@workflow.signal
def approve(self) -> None:
self.approved = TrueA signal is durable and asynchronous. Authorization belongs in the service that sends it through a workflow handle.
Reject an invalid update validate-workflow-update
@workflow.update
def set_limit(self, value: int) -> int:
previous = self.limit
self.limit = value
return previous
@set_limit.validator
def validate_limit(self, value: int) -> None:
if value < 0:
raise ValueError('limit must be non-negative')The validator must be deterministic and cannot change workflow state; the update handler may mutate state and return a result.
Create a sortable UUID during replay generate-deterministic-uuid7
from temporalio import workflow
request_id = workflow.uuid7()workflow.uuid7() is new in 1.32.0 and derives from workflow time plus deterministic randomness. Standard-library uuid7 is restricted in the sandbox.
Cap retries for a permanent effect bound-activity-retries
from datetime import timedelta
from temporalio.common import RetryPolicy
result = await workflow.execute_activity(
send_invoice,
invoice,
start_to_close_timeout=timedelta(seconds=30),
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=1),
maximum_attempts=5,
),
)Five attempts is a business decision, not a safe default. Mark permanent application failures non-retryable.
Checkpoint progress and receive cancellation heartbeat-long-activity
from temporalio import activity
@activity.defn
def import_rows(rows: list[str]) -> int:
start = next(iter(activity.info().heartbeat_details), 0)
for index, row in enumerate(rows[start:], start=start):
write_row(row)
activity.heartbeat(index + 1)
return len(rows)Schedule this with heartbeat_timeout. A synchronous activity observes cancellation when it heartbeats.
Serialize Pydantic 2 models use-pydantic-converter
from temporalio.client import Client
from temporalio.contrib.pydantic import pydantic_data_converter
client = await Client.connect(
'localhost:7233',
data_converter=pydantic_data_converter,
)The built-in converter supports Pydantic 2, not Pydantic 1. Version 1.32.0 caches up to 1024 type adapters per converter by default.
Test new code against stored history replay-production-history
from temporalio.client import WorkflowHistory
from temporalio.worker import Replayer
history = WorkflowHistory.from_json(workflow_id, history_json)
await Replayer(workflows=[Checkout]).replay_workflow(history)Replay representative production histories before a deploy to catch nondeterministic command changes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| celery | PyPI | Choose it for broker-backed jobs when durable event-history replay and workflow signals are unnecessary. |
| prefect | PyPI | Choose it when Python data pipelines, scheduling, retries, and an operations UI are the main problem. |
| durabletask | PyPI | Choose it when an existing Microsoft Durable Task backend and its orchestration model are already part of the stack. |
More infra guides
boto3 · opentelemetry-api · psutil · distro · @opentelemetry/api · google-cloud-storage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

