What is Temporal?
Temporal (temporal.io) is a durable execution platform for orchestrating long-running, fault-tolerant workflows. Think of it as “code that survives crashes, restarts, and network failures without you writing the retry/state-recovery logic yourself.”
Originally forked from Uber’s Cadence. SDKs exist for Go, Java, Python, TypeScript, .NET, PHP, Ruby.
The problem it solves
If you’ve ever written code like this in a distributed system:
def process_order(order_id):
charge_card(order_id) # step 1 — what if this succeeds but...
reserve_inventory(order_id) # step 2 — fails here?
send_confirmation(order_id) # step 3 — and the process crashes?
You hit the same problems over and over:
- Need to persist state somewhere (DB row tracking progress).
- Need to retry failed steps without re-running successful ones.
- Need to handle timeouts, cancellation, compensating transactions.
- Need to survive worker restarts, deploys, network partitions.
Most teams reinvent this with Celery + DB tables + cron jobs + ad-hoc state machines. Temporal takes that pattern and makes it the platform.
Core concepts
| Concept | What it is |
|---|---|
| Workflow | The orchestration code — deterministic, defines the what and when |
| Activity | The “do the actual work” code — non-deterministic, calls external systems |
| Worker | A process that polls task queues and executes workflows/activities |
| Task Queue | Named queue that workers poll; routes tasks to appropriate workers |
| Temporal Service | The server cluster (history, matching, frontend, worker services) backed by Cassandra/PostgreSQL/MySQL |
| Event History | Append-only log of every workflow event — the source of durability |
Durable execution — the key idea
Temporal records every step a workflow takes in an event history. If a worker crashes mid-workflow:
- Another worker picks up the workflow.
- Temporal replays the event history to reconstruct workflow state in memory.
- Execution continues from where it left off — activities that already completed return their cached results; only the unfinished step actually runs again.
This is why workflows must be deterministic — the same history must produce the same code path on replay.
from temporalio import workflow, activity
from datetime import timedelta
@activity.defn
async def charge_card(order_id: str) -> str:
# The actual side-effect — calls Stripe, can fail, is retried
return await stripe.charge(order_id)
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order_id: str):
# Workflow code: orchestration only. No I/O. No random. No clock.
charge_id = await workflow.execute_activity(
charge_card,
order_id,
start_to_close_timeout=timedelta(seconds=30),
retry_policy=RetryPolicy(maximum_attempts=5),
)
await workflow.execute_activity(reserve_inventory, order_id, ...)
await workflow.execute_activity(send_confirmation, order_id, ...)
return charge_id
If the worker dies after charge_card succeeds but before reserve_inventory starts, a new worker replays the history, sees charge_card already returned charge_id, and resumes at reserve_inventory. The card is not charged twice.
What you get for free
- Retries with exponential backoff — configured declaratively per activity.
- Timeouts —
start_to_close,schedule_to_start,schedule_to_close,heartbeat. - Compensating actions — workflows can run sagas naturally with try/except.
- Long-running workflows — workflows can wait days, weeks, years. State is persisted, not held in worker memory.
- Versioning — old workflow instances continue running old code; new instances run new code (
workflow.patched()). - Visibility — query running workflows, inspect history, signal them.
- Scheduling — built-in cron-style scheduling without external scheduler.
Architecture
+---------------+ +-----------------+ +----------+
| Your code | gRPC | Temporal Server | gRPC | Workers |
| (Client SDK) +<------>+ (matching, +<------>+ (your |
| starts wf | | history, | | Python |
+---------------+ | frontend, | | code) |
| worker svc) | +----------+
+--------+--------+
|
+-------v--------+
| Persistence |
| (Cassandra / |
| PostgreSQL / |
| MySQL) |
+----------------+
Workers poll task queues — Temporal Server doesn’t push to them. That means no inbound ports on workers, easy horizontal scaling.
When to use Temporal
Good fit:
- Multi-step business processes (order fulfillment, onboarding, KYC, billing cycles).
- Sagas / distributed transactions with compensations.
- Long-running jobs (hours to months) — provisioning, batch ETL, scheduled work.
- Workflows requiring human-in-the-loop steps (waiting for approval).
- AI agent orchestration with retries and tool calls.
Overkill for:
- Simple fire-and-forget background tasks (use Celery / RQ).
- Pure streaming / high-throughput event processing (use Kafka + consumers).
- Request/response RPC (use HTTP/gRPC directly).
Python SDK quickstart
# worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
async def main():
client = await Client.connect("localhost:7233")
worker = Worker(
client,
task_queue="orders",
workflows=[OrderWorkflow],
activities=[charge_card, reserve_inventory, send_confirmation],
)
await worker.run()
asyncio.run(main())
# starter.py
client = await Client.connect("localhost:7233")
handle = await client.start_workflow(
OrderWorkflow.run,
"order-123",
id="order-workflow-order-123",
task_queue="orders",
)
result = await handle.result() # blocks until workflow completes
Common gotchas
- Non-determinism in workflows — using
datetime.now(),random.random(),requests.get(), or threading inside@workflow.defnwill break replay. Useworkflow.now(),workflow.random(), and put all I/O in activities. - Imports at workflow level — heavy imports run on every replay. Use the sandbox passthrough for libraries that aren’t deterministic-safe.
- Big payloads — activity inputs/outputs go through the history. Large blobs (> 2MB by default) get rejected; pass references (S3 URLs) instead.
- History size limit — workflows have a 50k event history cap (configurable). Long-running workflows should use
continue_as_newto start a fresh history.
Hosted vs self-hosted
- Temporal Cloud — managed SaaS by Temporal Technologies. Pay per action.
- Self-hosted — open source, run on your own Kubernetes/VMs. Operations burden is real (Cassandra/Elasticsearch ops).
See 02_workflows_vs_activities.md for the deterministic vs non-deterministic distinction, 03_temporal_vs_celery.md for comparisons to other tools, and 04_temporal_interview.md for typical interview questions.
Interview angle
- “What is Temporal and what problem does it solve?” — durable execution platform; eliminates the need to manually persist workflow state, write retry logic, and handle worker crashes for multi-step business processes.
- “How does Temporal achieve durability?” — every workflow event is persisted to an append-only history. On worker failure, another worker replays the history to reconstruct state and continues from the last checkpoint.
- “Why must workflow code be deterministic?” — Temporal reconstructs workflow state by replaying its history through the workflow code. If the code branches differently on replay (e.g., using current wall-clock time), the replay diverges from the original execution.