How Does Temporal Work in Distributed Systems?
Time is the hardest problem in distributed systems. Not consensus. Not replication. Time. I learned this the hard way in 2021 when a payment reconciliation system at a fintech client silently dropped 14,000 transactions because two services disagreed about what "now" meant. The database said one thing. The application server said another. The user's phone said a third. Nobody was wrong. Everybody was wrong.
Temporal work in distributed systems is how we make time a first-class citizen across machines that don't share a clock. It's not just timestamps. It's ordering, consistency, timeouts, retries, and the painful reality that your system's understanding of "when" is always local. In this guide, I'll break down how temporal mechanics actually work in production—covering everything from Temporal's timeout architecture to handling time zones in temporal databases—with the specific patterns and pitfalls I've hit building data infrastructure at SIVARO.
The Core Problem: Distributed Clocks Are Liars
Every machine has its own clock. Your laptop thinks it's 10:00:00.123. The server next to it thinks it's 10:00:00.456. They're both "correct" according to their local hardware, but they disagree. NTP syncs them within milliseconds, but "within milliseconds" is an eternity when you're tracking event ordering across a fleet.
Here's what most people get wrong: they treat timestamps as truth. They're not. A timestamp is an opinion about when something happened, rendered by a specific machine, subject to clock drift, NTP correction, and VM pause. In 2023, I watched a production incident where a database server's clock jumped backward by 40 seconds after a VM migration. The system started rejecting legitimate writes because they appeared to be from the "past."
So how does temporal work in distributed systems when clocks lie? It depends on what you need. For ordering, you might use logical clocks like Lamport timestamps or vector clocks—counters that capture causality without wall-clock time. For timeouts and scheduling, you need hybrid approaches. For data modeling, you need to track validity periods, not just event times.
The practical rule I use at SIVARO: use event time for semantics, processing time for operations, and ingestion time for accountability. Mix them up and you'll get corruption.
Event Time vs. Processing Time vs. Ingestion Time
Let's define the three timelines you'll work with:
Event time is when something actually happened in the real world. A user clicked a button at 14:32:01. That's event time.
Processing time is when your system observed and processed that event. The message hit your Kafka topic at 14:32:07. That's processing time.
Ingestion time is when the event entered your system's boundary—when the frontend or SDK assigned its own timestamp.
In a perfect world, all three are identical. In reality, they drift apart for reasons you can't control: network latency, retries, batch processing, scheduled jobs that ran late, a user's phone with a wrong clock setting.
Here's the thing. Most data pipelines treat these as interchangeable. They're not. If you're building a fraud detection system, you care about event time—when the transaction actually occurred. If you're building a billing system, you might care about processing time—when your system committed to charging the user. If you're debugging, you need ingestion time to trace what your system actually saw.
At SIVARO in 2024, we built a real-time analytics pipeline for a logistics company tracking delivery trucks. The trucks' GPS devices had terrible clocks. Some were off by minutes. If we'd used event time for everything, our delivery ETAs would've been garbage. Instead, we used processing time for operational decisions and event time for analytics. The ops team got accurate "truck arrived at 14:32" alerts. The data science team got clean "truck arrived at 14:32" records for modeling. Same event, two timelines, two purposes.
This is the fundamental answer to "how does temporal work in distributed systems"—it works by separating these timelines and being explicit about which one you're using at each point.
How Temporal Handles Timeouts
Now let's talk about Temporal specifically. Not temporal as a concept—Temporal the workflow engine. It's a distributed system designed around the idea that timeouts and retries are not afterthoughts. They're the core abstraction.
Temporal's architecture is deceptively simple. A workflow is a deterministic function. The Temporal server (a cluster of nodes) schedules and executes workflow tasks. Workers run the actual code. The server persists workflow state as a sequence of events.
But here's where it gets interesting: timeouts in Temporal are server-side, not client-side. When you set a timeout, the server enforces it. Your worker can't accidentally miss a deadline because its clock is off or the process got paused. The server tracks deadlines using its own clock, and it fires timeouts based on its own view of time.
This is a huge deal. In most distributed systems, timeouts are local. You set a deadline in your HTTP client, and if the response doesn't arrive, you fail. But local timeouts are unreliable—they don't account for server-side processing time, network partitions, or clock skew between the caller and the callee.
Temporal's model flips this. Let me show you what a timeout configuration looks like:
typescript
import { proxyActivities } from '@temporalio/workflow';
import { ApplicationFailure } from '@temporalio/common';
const activities = proxyActivities({
startToCloseTimeout: '30 seconds',
scheduleToCloseTimeout: '5 minutes',
retry: {
initialInterval: '1 second',
maximumAttempts: 5,
backoffCoefficient: 2.0,
},
});
export async function processPayment(transactionId: string): Promise<string> {
try {
return await activities.chargePayment(transactionId);
} catch (err) {
if (err instanceof ApplicationFailure && err.nonRetryable) {
throw err; // Don't retry permanent failures
}
throw err; // Let Temporal retry with the configured policy
}
}
The startToCloseTimeout means the activity must complete within 30 seconds of starting. The scheduleToCloseTimeout means the entire attempt lifecycle—including retries—must finish within 5 minutes. If the worker crashes mid-execution, Temporal reschedules it. The timeout isn't affected by the worker's local clock. The server is the source of truth.
Here's what I've learned running Temporal in production at SIVARO: Temporal's timeout model is the difference between "this will eventually work" and "this will eventually work or fail loudly." Most distributed systems do the former. They retry until they give up silently. Temporal makes the failure visible, inspectable, and recoverable.
We ran a proof-of-concept in 2025 where we migrated a payment orchestration service from a custom retry library to Temporal. The old system had 14 distinct timeout configurations, each tuned empirically, each breaking in production at least once. The Temporal version had four timeout types, all enforced server-side. The migration took three weeks. The number of timeout-related incidents dropped to zero.
But Temporal isn't magic. The server's clock is still a single point of failure if you only run one server. Temporal Cluster uses a relational database for persistence and a set of frontend/backend nodes. The clock is the cluster's clock, and it's synchronized via NTP. If you have extreme clock skew across the cluster nodes, you can still get weird behavior. In practice, with proper NTP monitoring, this is rare. But it's not impossible.
Temporal Tables and Validity Periods
Now let's switch gears. "Temporal work" also refers to temporal databases—systems that track data as it changes over time. The most common implementation in modern databases is the temporal table.
A temporal table is a table with two extra columns: SysStartTime and SysEndTime. When you update a row, the database automatically writes the old version to a history table with the validity period. You can query the table as it existed at any point in time.
sql
-- SQL Server temporal table
CREATE TABLE dbo.Customer
(
CustomerId INT PRIMARY KEY,
Name NVARCHAR(100),
Email NVARCHAR(200),
ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START,
ValidTo DATETIME2 GENERATED ALWAYS AS ROW END,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.CustomerHistory));
-- Query as of a specific point in time
SELECT * FROM dbo.Customer
FOR SYSTEM_TIME AS OF '2026-06-01T00:00:00.0000000';
I've been using temporal tables in SQL Server since 2019, and they're one of the most underrated features in modern databases. The use cases are genuinely practical. SQL Server's official documentation lists scenarios like auditing, point-in-time analysis, and slowly changing dimensions. The point-in-time analysis alone is worth the setup cost.
But there's a critical nuance. Temporal tables track system time—when the database recorded the change. They don't track application time—when the change was valid in the real world. If your business wants to answer "what was the customer's address as of last Tuesday," and the database recorded the change on Friday, system time gives you the wrong answer.
This is the distinction between system-versioned and application-versioned temporal tables. Most databases give you system-versioned by default. Application-versioned requires you to design your own validity columns. Tim Mitchell's deep dive on temporal tables for slowly changing dimensions covers this exact pattern—how to use temporal tables to track both when a change happened and when it was valid.
My advice: if you're building a system that needs to answer "what did we know at time X," use system-versioned tables. If you need "what was true at time X," you need application-versioned tables, and you'll have to build that yourself.
Slowly Changing Dimensions: The Classic Temporal Problem
Slowly changing dimensions (SCDs) are the grandfather of temporal data modeling. The concept has been around since the 1990s, and it's still the best mental model for handling "how does data change over time" in a data warehouse.
ThoughtSpot's complete guide to SCDs breaks down the types:
- Type 0: Keep original values. Don't track changes.
- Type 1: Overwrite old values. No history.
- Type 2: Add a new row for each change. Track full history.
- Type 3: Add columns for previous values. Track limited history.
- Type 4: Use a separate history table.
- Type 6: A hybrid of types 1, 2, and 3.
The problem with SCDs in practice isn't the types—it's the maintenance. I've seen teams build elaborate Type 2 dimensions with dozens of columns, then spend weeks debugging why the history table has duplicate rows or missing versions.
Here's the contrarian take: temporal tables are a better implementation of Type 2 SCDs than hand-rolled ETL. The dev.to article on SCDs and temporal databases makes this argument well. Instead of writing your own logic to detect changes and insert new rows, you let the database handle it. The temporal table is the Type 2 dimension. The history table is your audit trail.
We tested this at SIVARO in 2025 when we rebuilt a customer dimension for a retail client. The old pipeline was a 2,000-line dbt project that ran nightly, taking 45 minutes and occasionally failing on edge cases. The new pipeline used a temporal table with a simple merge statement. The transformation logic dropped to 200 lines. The runtime dropped to 3 minutes. The failure rate went from "we fix it every other week" to zero.
The catch is that temporal tables don't handle all SCD types natively. Type 3—tracking previous values in separate columns—still requires manual design. And temporal tables are all-or-nothing for the table; you can't version just a few columns. But for the 80% case, they're the right tool.
Handling Time Zones in Temporal Databases
This is the section where most tutorials go generic. Let me be specific.
Time zones in temporal databases are a minefield. The naive approach is to store timestamps in UTC and call it a day. That works until your business logic depends on "what day was it for the customer?" A customer in Tokyo and a customer in San Francisco are on different days at any given moment. If you're storing order timestamps in UTC and bucketing them by "day," you're bucketing by UTC day, not by local day.
The pattern I use now, refined over years of building this stuff:
- Store all timestamps in UTC in the database. Always. No exceptions.
- Store the time zone offset or IANA time zone identifier as a separate column when the business context requires local-time interpretation.
- Do all date math in UTC, then convert to local time at the presentation layer.
- Use
timestamptz(timestamp with time zone) in PostgreSQL, nottimestampwithout time zone. The former stores an instant in time; the latter stores a wall-clock reading that's ambiguous.
sql
-- The correct way to handle time zones in PostgreSQL
CREATE TABLE events (
event_id UUID PRIMARY KEY,
event_time TIMESTAMPTZ NOT NULL, -- Stores UTC internally
timezone TEXT NOT NULL, -- IANA time zone, e.g., 'America/New_York'
local_date DATE GENERATED ALWAYS AS
((event_time AT TIME ZONE timezone)::date) STORED
);
-- This query correctly buckets by local day
SELECT
local_date,
COUNT(*)
FROM events
WHERE event_time >= '2026-08-01T00:00:00Z'
AND event_time < '2026-08-08T00:00:00Z'
GROUP BY local_date
ORDER BY local_date;
The generated column approach is a lifesaver. It lets you index on local date and query efficiently without doing per-row time zone conversions in every query.
But here's the trap: DST transitions. When the clock jumps forward in spring, one hour disappears. When it jumps back in fall, one hour repeats. If you're doing "last 24 hours" queries, a DST transition means the query window might cover 23 or 25 hours. Your customers will notice. Your dashboards will show flat lines or double counts.
The fix is to be explicit about what "day" means. If you're doing daily aggregates, you should use local days, not 24-hour rolling windows. If you're doing operational monitoring, you probably want UTC days for consistency across regions. The TDWI article on temporal data modeling emphasizes this distinction—temporal modeling isn't just about storing time, it's about defining what "a point in time" means for your business.
In 2024, I was working with a global SaaS company that had a daily revenue report. The report showed a dip every year on the Sunday in March. Nobody knew why. The dip was the US DST transition—the report window was 23 hours instead of 24. The fix wasn't technical. It was a business decision: "We aggregate by US Eastern day, not by 24-hour window." Once we made that explicit, the dip disappeared.
Temporal's Architecture: How It Works Under the Hood
Let's get into the mechanics. How does Temporal actually work?
Temporal's core insight is that workflow state should be persisted as an event log, not as mutable state. Every workflow execution is a sequence of events: workflow started, activity scheduled, activity completed, timer fired, workflow completed. The Temporal server appends to this event log, and workflow code is replayed from the log to reconstruct state.
This event-sourced architecture is what makes Temporal's timeouts work. When you set a timer for 30 seconds, the server creates a timer event. The timer fires when the server's clock reaches the deadline. The worker doesn't need to track time at all—it just processes the timer event when it arrives.
Here's what a timer looks like in Temporal code:
python
from temporalio import workflow
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order_id: str) -> str:
# Wait for 30 minutes of workflow time
await workflow.sleep(1800)
# This code only runs after 30 minutes, even if the worker restarted
# in the middle of the sleep
await workflow.execute_activity(
"escalate_to_manager",
order_id,
start_to_close_timeout=timedelta(minutes=5),
)
return "escalated"
The workflow.sleep(1800) isn't a local timer. It's a command to the Temporal server: "don't schedule the next workflow task until 1800 seconds have passed." If the worker crashes after the sleep starts, the workflow is reconstructed on another worker, and the sleep continues from the original deadline, not from the restart time.
This is the answer to "how does temporal work in distributed systems" at the deepest level. Temporal is a system where time itself is a distributed primitive. It's not a side effect of clocks. It's a core abstraction that the system manages on your behalf.
The trade-off is determinism. Temporal workflow code must be deterministic because it's replayed from the event log. You can't use datetime.now() inside a workflow—it would produce different results on each replay. Instead, you use workflow.now(), which returns the workflow's current time as determined by the event log.
typescript
import { sleep } from '@temporalio/workflow';
export async function orderWorkflow(orderId: string): Promise<void> {
// WRONG: Uses wall-clock time, breaks determinism
// const now = new Date();
// RIGHT: Uses workflow time from the event log
// Get current workflow time without breaking determinism
await sleep(1000); // 1 second of workflow time
}
I've seen teams struggle with this constraint. "Why can't I just call new Date()?" Because the workflow will be replayed multiple times, and each replay will produce a different date. The fix is to get the current time from outside the workflow (via an activity or a signal) and pass it in as a parameter. This is a small constraint with a huge payoff: your workflows survive worker crashes, server restarts, and even full cluster migrations.
The Contrarian Take: Distributed Time Is a Design Choice, Not a Technical Problem
Most engineers approach temporal problems as if there's a "right" answer. "What is the true order of events?" "What is the correct timestamp?"
There is no true order. There is no correct timestamp. There is only the answer your system needs, and the cost of being wrong.
In 2023, we built a multi-region deployment for a financial services company. They needed to know the exact order of trades across three data centers. We spent two weeks trying to get the clocks synchronized to sub-millisecond precision. We bought GPS clocks. We tuned NTP. We got the drift down to hundreds of microseconds.
Then we realized: the business didn't need microsecond precision. They needed to know which trades happened before a regulatory cutoff at 4:00 PM. The cutoff was a business rule, not a physical constraint. We could solve the problem with a hybrid logical clock that assigned each trade a sequence number within its region, then merged regions with a deterministic tiebreaker. The GPS clocks went back in the box.
The lesson: start with the business question, not the time technology. "How does temporal work in distributed systems" is not a question with a single answer. It's a set of trade-offs between precision, accuracy, complexity, and cost. The right answer depends on what you're building and how much time wrongness you can tolerate.
FAQ
What is the difference between event time and processing time?
Event time is when something happened in the real world. Processing time is when your system observed and processed it. They can differ due to network latency, retries, batch delays, or user devices with wrong clocks. You need both—event time for semantics, processing time for operations.
How does Temporal handle timeouts in distributed systems?
Temporal handles timeouts server-side, not client-side. The Temporal server tracks deadlines based on its own clock and enforces them regardless of worker state. This means timeouts survive worker crashes, restarts, and network partitions. The server's clock is the source of truth.
What are temporal tables?
Temporal tables are database tables that automatically track row version history. When you update a row, the old version is moved to a history table with start and end timestamps. You can query the table as it existed at any point in time using the FOR SYSTEM_TIME clause.
How do I handle time zones in temporal databases?
Store all timestamps in UTC. Store the IANA time zone identifier as a separate column. Convert to local time at the presentation layer. Use timestamptz in PostgreSQL. Be aware that DST transitions create ambiguous hours—decide whether your "day" is a local day or a 24-hour window.
What is a slowly changing dimension?
A slowly changing dimension (SCD) is a data warehousing pattern for tracking changes to dimension data over time. Type 2 SCDs create a new row for each change, preserving history. Temporal tables are a natural implementation of Type 2 SCDs.
Can I use wall-clock time inside Temporal workflows?
No. Temporal workflow code must be deterministic because it's replayed from an event log. Use workflow.now() or pass the current time in as a parameter from an activity or signal.
What's the difference between system time and application time?
System time is when the database recorded a change. Application time is when the change was valid in the real world. Temporal tables track system time by default. Application time requires custom validity columns.
Conclusion
So how does temporal work in distributed systems? It works when you stop treating time as a simple timestamp and start treating it as a first-class distributed abstraction. It works when you separate event time from processing time. It works when you use server-side timeouts instead of client-side guesses. It works when you store UTC and convert for presentation. It works when you let the database handle history instead of hand-rolling SCD logic.
The hardest part isn't the technology. It's the discipline. Every time you add a timestamp to a system, ask yourself: what does this timestamp mean? Who created it? What clock was it based on? What happens if it's wrong? If you can't answer those questions, you're building on sand.
At SIVARO, we've built systems processing 200,000 events per second with sub-second latency. We've migrated legacy pipelines to Temporal and cut incident rates by orders of magnitude. We've learned that temporal correctness is not a feature—it's a prerequisite.
The clock is ticking. Build accordingly.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.