AI News Hub Logo

AI News Hub

Temporal.io: Stop Losing State When Your Server Crashes (Production Guide)

DEV Community
Atlas Whoff

Background jobs are a solved problem until they aren't. A job starts, your server restarts during a deploy, and you're left with half-processed records and no way to resume. Temporal solves this by storing workflow state in a database and replaying from checkpoints. Here's how it works in practice. // Without Temporal — fragile async function processOrder(orderId: string) { await chargeCard(orderId) // ✅ await fulfillOrder(orderId) // server crashes here await sendConfirmationEmail() // never runs await updateInventory() // never runs } If your server crashes between steps 2 and 3, you have a charged card and no fulfillment. Recovering requires a separate audit job, idempotency keys, and manual intervention. // With Temporal — durable export async function processOrderWorkflow(orderId: string) { await workflow.executeActivity(chargeCard, orderId) await workflow.executeActivity(fulfillOrder, orderId) await workflow.executeActivity(sendConfirmationEmail, orderId) await workflow.executeActivity(updateInventory, orderId) } If the worker crashes between steps 2 and 3, Temporal replays from the last checkpoint. No duplicate charges, no lost state. # Run Temporal server locally docker run -d -p 7233:7233 temporalio/auto-setup:latest # Install SDK npm install @temporalio/worker @temporalio/client @temporalio/workflow Activities are the actual side effects — API calls, DB writes, emails. // src/activities/orderActivities.ts import type { ActivityInterface } from "@temporalio/activity" export interface OrderActivities { chargeCard(orderId: string): Promise fulfillOrder(orderId: string): Promise sendConfirmationEmail(orderId: string): Promise updateInventory(orderId: string): Promise } export const orderActivities: OrderActivities = { async chargeCard(orderId) { const order = await db.orders.findById(orderId) await stripe.charges.create({ amount: order.amount, currency: "usd", customer: order.stripeCustomerId }) await db.orders.update({ id: orderId, data: { charged: true } }) }, async fulfillOrder(orderId) { await fulfillmentApi.processOrder(orderId) await db.orders.update({ id: orderId, data: { fulfilled: true } }) }, async sendConfirmationEmail(orderId) { const order = await db.orders.findById(orderId) await resend.emails.send({ to: order.customerEmail, subject: "Order confirmed", react: OrderConfirmation({ orderId }) }) }, async updateInventory(orderId) { const items = await db.orderItems.findByOrder(orderId) for (const item of items) { await db.inventory.decrement({ productId: item.productId, qty: item.quantity }) } } } Workflows are deterministic functions that orchestrate activities. No direct I/O — everything goes through activities. // src/workflows/processOrder.ts import { proxyActivities } from "@temporalio/workflow" import type { OrderActivities } from "../activities/orderActivities" const { chargeCard, fulfillOrder, sendConfirmationEmail, updateInventory } = proxyActivities({ startToCloseTimeout: "5 minutes", retry: { maximumAttempts: 3, initialInterval: "1 second", backoffCoefficient: 2 } }) export async function processOrderWorkflow(orderId: string): Promise { await chargeCard(orderId) await fulfillOrder(orderId) await sendConfirmationEmail(orderId) await updateInventory(orderId) } Each activity gets automatic retry with exponential backoff. If chargeCard fails 3 times, the workflow fails — it doesn't silently skip. // src/worker.ts import { Worker } from "@temporalio/worker" import { orderActivities } from "./activities/orderActivities" const worker = await Worker.create({ workflowsPath: require.resolve("./workflows/processOrder"), activities: orderActivities, taskQueue: "orders" }) await worker.run() Workers can be scaled horizontally. Multiple workers can pick up tasks from the same queue. // app/api/checkout/route.ts import { Client, Connection } from "@temporalio/client" import { processOrderWorkflow } from "@/workflows/processOrder" const connection = await Connection.connect({ address: "localhost:7233" }) const client = new Client({ connection }) export async function POST(req: Request) { const { orderId } = await req.json() const handle = await client.workflow.start(processOrderWorkflow, { args: [orderId], taskQueue: "orders", workflowId: `order-${orderId}` // idempotent — same ID = same workflow }) return Response.json({ workflowId: handle.workflowId }) } The workflowId makes this idempotent. If the checkout endpoint is called twice with the same orderId, Temporal deduplicates — the workflow runs once. Workflows can receive signals mid-execution: import { defineSignal, setHandler } from "@temporalio/workflow" const cancelOrder = defineSignal("cancelOrder") export async function processOrderWorkflow(orderId: string) { let cancelled = false setHandler(cancelOrder, () => { cancelled = true }) await chargeCard(orderId) if (cancelled) { await refundCard(orderId) return } await fulfillOrder(orderId) // ... } // From your API — send a signal to a running workflow await client.workflow.getHandle(`order-${orderId}`).signal(cancelOrder) Use Temporal for: Multi-step processes where partial completion = real damage (payments, fulfillment) Long-running jobs (hours/days) that must survive deploys Human-approval workflows with timeouts Saga patterns across microservices Skip Temporal for: Simple fire-and-forget jobs (use BullMQ) Jobs that are naturally idempotent and fast (<1 minute) Solo dev projects where operational complexity isn't worth it The Temporal server adds an infrastructure dependency. For small projects, BullMQ with Redis is simpler. Temporal's value compounds as your workflow complexity grows. Building a SaaS with complex async workflows? The AI SaaS Starter Kit ships with a production-ready background job setup and Stripe billing — get to production in a weekend instead of two weeks.