backend / message queues / temporal / 01_what_is_temporal.md

What is Temporal?

3 interview angles 5 min read source

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:

  1. Another worker picks up the workflow.
  2. Temporal replays the event history to reconstruct workflow state in memory.
  3. 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.
  • Timeoutsstart_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.defn will break replay. Use workflow.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_new to 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.