A request leaves one service at 10:00:02 and arrives at another at 10:00:01. Your logs appear to show a response travelling backwards through time. In a different part of the application, a five-minute reservation expires early even though its expiry calculation looks perfectly reasonable.
Both problems can come from treating separate computer clocks as though they were one shared clock. Most application code can ignore tiny differences for long periods, which makes the failures particularly surprising when an adjustment, a poorly synchronised machine or a strict time comparison finally exposes them.
Introduction
Time answers several different questions in an application. You may want to know when an order was created, how long a request took, whether a reservation has expired or which update happened first.
Those questions are related, but they do not all need the same kind of clock. A calendar timestamp is useful for displaying an order date. A local elapsed-time counter is better for measuring a request. An explicit version or causal relationship can be more reliable than either when deciding the order of updates.
We will explore these distinctions using a small application with an API, a payment service and a background worker. The aim is not to build a precision time service. It is to recognise where everyday application code assumes more agreement than separate machines can promise, then choose an appropriate way to handle that uncertainty.
Every Server Has Its Own View of the Time
A computer maintains its estimate of the current time using local hardware and operating-system facilities. Time synchronisation software adjusts that estimate using other sources, often across a network.
Two machines can therefore report slightly different times at the same real instant. The difference between their readings is often called clock skew or clock offset. Clock drift describes a clock running at a slightly different rate, so that its error changes over time.
The distinction matters. A machine that is consistently two seconds ahead creates one kind of problem. A machine whose clock gradually gains time creates another. Corrections can also change the apparent passage of calendar time while the application is running.
Network synchronisation helps, but messages take time to travel. The time source's reply arrives after it was sent, and the outward and return journeys may take different amounts of time. The local machine must estimate the relationship rather than observe an instantaneous shared clock.
The Network Time Protocol specification describes mechanisms for estimating offsets, delays and clock behaviour. Applications still need to work within the accuracy and failure characteristics of their environment.
The practical conclusion is modest: a timestamp is a useful observation from a particular clock. It is not automatically an exact statement of universal event order.
UTC Removes a Conversion Problem, Not Clock Error
Storing timestamps in UTC is a sensible default for events that represent an actual instant. It avoids confusing a London time with a New York time and makes exchange between services easier.
However, UTC describes the time standard and representation. It does not prove that the machine producing the timestamp is accurate. Two servers can both emit timestamps ending in Z while disagreeing by several seconds.
Consider these messages:
API: 2030-04-12T10:00:02Z Payment request sent
Payment: 2030-04-12T10:00:01Z Payment request received
There is no timezone conversion error here. If the API clock is ahead of the payment server's clock, a perfectly ordinary network exchange can produce this ordering.
Do not “fix” the record by silently changing the received timestamp to be later than the sent timestamp. That invents precision and removes evidence of the clock difference. Keep the observations and use request relationships to reconstruct what happened.
Also preserve the distinction between an instant and a business date. “Delivery on Tuesday” may be a date in the customer's chosen timezone rather than a timestamp at midnight UTC. Giving every time-related field the same storage type can blur these separate meanings before clock skew even enters the picture.
Separate Wall-Clock Time from Elapsed Time
Wall-clock time is the current date and time that people recognise. It can answer “when did this start?” and support comparisons with a calendar deadline.
Elapsed time answers “how much time passed between these two observations here?” For this, platforms provide counters intended for duration measurement, commonly based on a monotonic clock. Monotonic means the clock does not move backwards within its supported measurement context.
Subtracting two wall-clock readings seems equivalent until the clock changes between them. Start at 10:00:00, perform work for two seconds, and imagine the calendar clock is adjusted backwards by five seconds. The subtraction now suggests a negative duration.
A forward adjustment can create the opposite error: a fast operation appears to have taken a long time. This can pollute latency metrics or cause application code to declare that a retry budget has already expired.
Use the platform's elapsed-time facilities for local durations. In .NET, Stopwatch is the standard API for measuring elapsed time. Understand the guarantees of the runtime and operating system you deploy, particularly for suspension or unusual virtualised environments.
Keep wall-clock timestamps alongside duration measurements when both are useful. The timestamp locates an operation in an incident timeline; the elapsed measurement describes how long that operation took locally.
Measure a Request with the Right API
A simple .NET measurement can keep those responsibilities separate:
var startedAt = DateTimeOffset.UtcNow;
var timer = Stopwatch.StartNew();
try
{
await ProcessOrderAsync(cancellationToken);
}
finally
{
timer.Stop();
logger.LogInformation(
"Order processing started at {StartedAt}; elapsed {ElapsedMs} ms",
startedAt,
timer.Elapsed.TotalMilliseconds);
}
The example assumes the usual namespace imports and application dependencies. The wall-clock value is captured for context, while Stopwatch measures the local interval. Logging in finally also records the duration when processing fails.
Do not send a raw stopwatch counter value to another server and subtract that server's counter from it. Counters may have different origins, frequencies or supported scopes. Their values are not a portable exchange format for calendar time.
Likewise, do not persist a raw local counter as a deadline to resume after an arbitrary restart. A later process or machine may not share the same measurement context. Durable business deadlines need an explicit representation and an authority responsible for interpreting them.
Choose names that communicate intent. CreatedAtUtc, ElapsedMilliseconds and OrderVersion tell future readers much more than three fields all called Timestamp. Good names make it harder to accidentally compare values that answer different questions.
Keep Timeout Budgets Local Where Possible
A timeout is usually a duration: allow this attempt up to two seconds. It should normally be enforced with the runtime's timer and cancellation facilities rather than a loop repeatedly subtracting wall-clock timestamps.
An end-to-end request may cross several services, so the remaining budget matters too. If the API already spent most of its allowed time, giving every downstream call a fresh full timeout can make the overall operation much longer than intended.
Within one process, track the remaining allowance using elapsed time and pass a bounded timeout to the next operation. Across processes, use the RPC framework's supported deadline propagation where available, and understand how it handles clock differences.
An absolute deadline such as “stop at 10:00:05 UTC” depends on the receiver's clock. A relative allowance avoids directly comparing two wall clocks, but it introduces another consideration: time spent travelling or waiting before receipt must still be accounted for if the promise is truly end to end.
Do not invent a universal solution by placing an unchecked duration in a header. Cap incoming budgets, preserve the caller's own deadline enforcement and avoid allowing each hop to restart the entire allowance.
Finally, cancellation is a request to stop work, not proof that a remote operation had no effect. Clock-correct timeout handling still needs the normal rules for retries and uncertain outcomes.
Give Durable Expiry a Clear Authority
Suppose a reservation expires five minutes after creation. The API writes ExpiresAtUtc, and a background worker later releases expired reservations.
If the worker's clock is ahead, it may release a reservation earlier than the API expects. If it is behind, availability may remain reserved too long. The problem is not arithmetic; it is which clock is allowed to decide the business transition.
For state held in one database, the owning service can centralise expiry decisions around that database's supported time functions and transactional checks. This reduces disagreement among application servers, although the database clock itself still needs proper management.
The final operation should verify the current reservation state and expiry condition atomically with the change. A worker that selected an expired row earlier must not cancel a reservation that was subsequently extended or confirmed.
Separate logical expiry from physical cleanup. A record may remain in storage after it is no longer valid because the cleanup process runs periodically. Reading the row's existence alone should not automatically imply that the reservation remains usable.
For a countdown displayed in the browser, accept that the display is an estimate. The authoritative service decides whether confirmation is still allowed. Return an understandable response when the deadline passes rather than trusting the customer's device clock to enforce the rule.
Do Not Resolve Concurrent Updates by Clock Alone
A tempting conflict rule is “keep the update with the latest timestamp”. It is easy to implement and produces one answer, but the answer depends on the clocks and on what “latest” means to the business.
Imagine two services updating a customer's contact preference. Server A's clock is five minutes ahead. Its earlier update receives a later timestamp than a real-world update performed afterwards on server B. A timestamp comparison can therefore restore the older preference.
Even perfectly synchronised clocks would not explain how to combine two simultaneous changes to different fields. Should changing a phone number overwrite an independently updated email address? That is a data model question, not a timekeeping question.
When one service owns a record, an explicit version can support conditional updates. A client submits the version it read; the service accepts the update only if the version still matches, or asks the client to resolve the conflict.
For event streams, a per-record sequence or ordered log position can tell a projection which update follows another within the relevant scope. Those numbers are assigned under a defined protocol instead of guessed from independent clocks.
Timestamp-based conflict resolution can still be an intentional product choice for suitable data. Document its limitations, including clock error and concurrent changes, rather than treating it as a general proof that the chosen value is correct.
Distinguish Event Time from Arrival Time
An event can have several useful times. A customer performs an action on a device, the server accepts it, the broker stores the event and a reporting worker processes it later.
Those moments answer different questions. Device time may describe when an offline user believes an action occurred. Server acceptance time describes when the system recognised it. Processing time describes when a particular consumer handled it.
If an offline device reconnects after a day, sorting solely by processing time makes yesterday's action look new. Sorting solely by an untrusted device timestamp lets a badly configured clock place an action far in the future.
Store the distinctions when they matter and explain their trust levels. A useful event might carry OccurredAt, AcceptedAt and a stable event identity, with an explicit statement of who supplied each timestamp.
Reports based on event time also need a policy for late arrivals. A daily total may change when an older event arrives after the initial calculation. Decide whether to revise the total, show an adjustment or close the reporting period under a documented rule.
Do not quietly discard every event with an unexpected timestamp. Preserve enough evidence to investigate whether it came from an offline client, a clock problem or invalid input. The appropriate business treatment depends on that distinction.
Reconstruct Cause and Effect Without Perfect Clocks
Some ordering facts do not require a clock. A service cannot receive a particular message before that message was sent. A response follows the request that caused it. Within a sequential operation, one step can depend on the result of another.
Carry identifiers that preserve those relationships. A trace identifier groups a request across services, while parent and child span relationships show which operation called another. Message causation references can identify the event or command that triggered later work.
Consider a payment request whose logs appear reversed because of skew. Matching the request identity still tells you that the API send caused the payment service receive. Local elapsed measurements can describe each side's work without subtracting their calendar timestamps.
This does not reveal every detail. Two independent requests may have no known causal order, and a trace can be incomplete if sampling or instrumentation omits a step. Avoid filling those gaps with certainty just because the log viewer sorts every line into one list.
A useful incident view preserves both the recorded timestamps and the causal relationships. Clock error then becomes something you can investigate, rather than a reason to distrust every other observation.
This way of thinking also improves application design: store the dependency that matters directly when the business already knows it.
Understand Logical Clocks at a Practical Level
A logical clock is a way to attach ordering information to events without claiming to measure seconds. The classic idea is associated with Leslie Lamport's paper on time and event ordering.
For a simplified example, each service keeps a counter. It advances the counter for relevant local events and includes its value in outgoing messages. On receipt, the other service advances beyond both its own counter and the received value.
If A sends a message labelled 7 while B's counter is 3, B can label the receive event 8. The labels respect the known dependency even if A and B's calendar clocks disagree.
The limitation is essential: a larger logical value does not prove that one unrelated business event happened later in physical time. It also does not tell you how many milliseconds passed. Logical ordering describes relationships established by the protocol.
For many ordinary applications, an aggregate version or a broker's partition position already supplies the ordering scope you need. You do not need to implement a general logical-clock scheme merely because your application uses messages.
Keep the scope explicit. Position 20 in one partition is not automatically later than position 19 in another. A number becomes useful ordering information because of the rules that produced it, not simply because it is numeric.
Treat Scheduling as a Separate Time Problem
“Run every twenty-four hours” and “run every day at 09:00 in London” describe different schedules. The first is based on an interval; the second is based on a local calendar and timezone rules.
Daylight-saving changes make that difference visible. Some local times occur twice or do not occur on a particular date. A scheduler needs a documented policy for those cases rather than assuming that adding twenty-four hours always produces the next intended local occurrence.
Persist the scheduling intent, including the timezone when the schedule is local. Storing only the next UTC instant can be useful for execution, but it does not necessarily preserve enough information to calculate every future occurrence correctly.
Also decide what happens after downtime. Should a missed daily report be produced on recovery, skipped or combined with later work? That decision should be based on the business schedule and durable execution history, not on how many timer callbacks the process happened to miss.
Clock adjustments can make a scheduler reconsider a time range. Use stable identities for intended occurrences, such as a schedule identity and its relevant business date, so that revisiting the range does not automatically repeat an already completed action.
Accurate timekeeping helps a scheduler wake at the right moment. It does not replace durable records of which scheduled business operations have already been accepted or completed.
Keep Time-Based Validation Explicit
Applications often validate time-limited credentials, signed requests and links. A verifier whose clock is substantially wrong can reject valid requests or accept credentials for longer than intended.
Use the established authentication library's validation facilities and documented clock-tolerance settings. A small tolerance can accommodate expected disagreement, but increasing it indefinitely weakens the meaning of expiry and can conceal an infrastructure problem.
Validate timestamps from clients according to their purpose. A client-supplied creation time may be useful display metadata while being unsuitable for deciding authorisation, payment deadlines or the age of a privileged request.
If many credentials suddenly appear to be issued in the future, investigate the issuer and verifier clocks before disabling validation. The symptom may come from a time source problem rather than an authentication rollout.
The same discipline applies to signed storage links and short-lived service credentials. Their expiry is interpreted by the system enforcing access, which may not be the machine showing a countdown to the user.
Make error reporting useful without exposing secrets. Record that validation failed because of timing and identify the relevant service or host. Avoid placing full credentials in logs while trying to diagnose an offset.
These checks are a reminder that time is sometimes part of a security decision. That makes the chosen authority and permitted uncertainty worth stating explicitly.
Test Clock Behaviour Without Waiting in Real Time
Code that directly reads the current time everywhere is difficult to test. A reservation test should not need to wait five minutes, and a scheduling test should not depend on the calendar date when the test happens to run.
Inject a time abstraction so tests can control the observations. .NET's TimeProvider overview describes access to wall-clock time, elapsed-time measurements and timers, together with a controllable testing implementation.
Test just before, exactly at and just after a deadline. Decide whether equality counts as expired and use that rule consistently. Also test a reservation being extended while a cleanup worker is preparing to act.
Use separate controllable clocks for separate services in integration tests. Giving every component the same fake time can hide the very disagreement the distributed workflow needs to tolerate.
For backward or discontinuous wall-clock changes, use an appropriate custom test provider or a controlled environment if the chosen fake implementation does not support that movement. Keep elapsed-time behaviour separate so that tests do not accidentally make every clock jump together.
Test delayed and reordered messages independently of clock skew. A perfectly synchronised system still receives old messages, and a skewed system can still deliver messages promptly. Combining those scenarios reveals whether the code relies on an accidental relationship between arrival order and timestamps.
Observe Clock Health and Handle Recovery Carefully
Monitor synchronisation health as part of the platform: offset estimates, loss of time sources and machines whose behaviour differs materially from their peers. The appropriate thresholds depend on the operations that rely on time.
A reporting application may tolerate a difference that breaks a short-lived credential check. A timer used for approximate cleanup has different requirements from a protocol whose safety depends on a bounded clock assumption.
If a host's time is badly wrong, follow the platform's supported correction procedure. A sudden wall-clock adjustment may affect running software, while a gradual correction may take time to restore accuracy. Understand that trade-off before changing clocks on production machines.
After recovery, inspect the consequences as well as the clock. Incorrectly future-dated records may still dominate timestamp-based conflict resolution. A scheduler may have recorded unexpected occurrences. Expiry decisions already committed to storage do not undo themselves when the clock becomes accurate again.
Preserve event identities and versions during repair. Rewriting all historical timestamps to “look sensible” can remove evidence and distort real business dates. Correct derived displays or affected state through documented rules based on the facts you can establish.
Healthy clocks reduce errors. Explicit application semantics limit the damage when healthy clocks temporarily become unavailable.
Work Through an Apparently Impossible Request
Imagine an API dashboard suddenly reports negative network latency for calls to the payment service. The calculation subtracts the API's send timestamp from the payment service's receive timestamp. Some results suggest that a request arrived nearly two seconds before it left.
Start by checking what the metric actually measures. It combines observations from two wall clocks, so it includes their offset as well as the real journey time. The negative value is evidence that the measurement is unsuitable, not evidence that the network behaved impossibly.
Follow one request using its trace identity. The API's local elapsed timer reports a round trip of 120 milliseconds. The payment service's local timer reports 20 milliseconds of processing. Those observations can both be reasonable even while the cross-server subtraction is negative.
You can say that the API spent 120 milliseconds waiting for the whole interaction. You cannot conclude from the two calendar timestamps exactly how long the outward network journey took. The remaining time around the payment service's measured work may include transport, connection handling, queuing and other unmeasured steps.
Next, examine the clocks of the specific hosts involved. Suppose the API host is around 1.8 seconds ahead while the payment host is synchronised normally. That explains the direction and approximate size of the impossible values. Check whether the problem is isolated to one machine or appears across a deployment group with a shared time configuration.
Correct the infrastructure issue through the supported operational procedure, but also fix the metric. Otherwise, a smaller future offset will continue producing inaccurate network measurements without being obvious enough to trigger investigation. Keep the local round-trip histogram and use tracing relationships to describe the cross-service sequence.
Then inspect any business logic using the same assumption. Perhaps the application also rejects a provider response if its timestamp appears earlier than the request. That validation would turn a harmless clock difference into a failed checkout. If the protocol already identifies the response securely by request identity, an invented timestamp-order requirement may add no useful protection.
Finally, add a test with the API clock ahead and another with it behind. Verify that the request still completes, the elapsed duration remains meaningful and the logs retain the original timestamps for diagnosis. A focused incident fix should remove the false assumption from both observability and behaviour, rather than merely hiding negative values by clamping them to zero.
Summary
Separate clocks can disagree even when every service uses UTC. Wall-clock timestamps describe calendar instants, elapsed-time counters measure local durations, and versions or causal relationships describe ordering within a defined scope.
Use the appropriate mechanism for each job. Measure request duration locally, give expiry a clear authority, preserve scheduling intent and avoid treating the newest timestamp as automatic proof of the newest business decision.
Test skew, delayed messages and boundary conditions explicitly. Monitor clock health, but also design for its limitations. An application that knows what each time value means can remain understandable and correct when its servers temporarily disagree about what time it is.
