ObservabilityCVIII · Clock SkewClockSkew
Effect on Logs
What you'll learn
- Predict the three wrong-time symptoms that clock skew produces in a log pipeline
- Explain why Loki rejects entries older than the per-tenant rejection window or out of stream order
- Identify the Loki distributor metric that flags a clock-driven rejection shape
- Configure the agent to extract a parseable source timestamp and to fail loud on a malformed line
- Trace a wrong-time entry from the source host through the distributor to the rejected entry in the ingester
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The application’s log says the request failed at 14:30:01. The database’s log says the same transaction committed at 14:30:03. The investigator opens Grafana, sets the time range to 14:30:00-14:30:05, and looks for the database-side trace of the request. The query is empty. The application host is one hundred and forty milliseconds behind the database host. The database committed at 14:30:03 wall-clock; the application saw 14:30:02.86 on its own clock. The investigator looked at the wrong time.
Three wrong-time symptoms appear in log pipelines under clock skew: a line that is older than the per-tenant window (the distributor silently rejects it); a line that is newer than the last line in the stream (the ingester rejects it as out of order); and a line that is in the right chunk but at the wrong position in the sorted timeline (the line is stored, the query is wrong). The common shape is the silent rejection at the distributor; the rare shape is the wrong-position entry that corrupts the investigation.
What it is
A log line carries two timestamps from the moment it is written to the moment it appears in Loki:
- Source timestamp. The wall-clock time the application stamped on the line, or the wall-clock time the kernel recorded when the line was written. For RFC3339 sources, this is the value in the line text. For syslog sources, this is the value the syslog parser extracts.
- Ingestion timestamp. The wall-clock time the line was accepted by the Loki distributor. This is the time the distributor stamped on the entry when the parser failed.
Loki stores the line against the source timestamp when the parser succeeds; otherwise it stores the line against the ingestion timestamp. The query path reads the stored timestamp. A wrong source timestamp therefore affects every downstream query: the line appears at the wrong time, in the wrong time range, and against the wrong retention boundary.
Loki 3.x accepts timestamps in RFC3339 with up to nanosecond precision. Unix epoch in seconds, milliseconds, or microseconds is also accepted. Local time without an explicit offset is the most common source of a wrong timestamp.
Why a sysadmin cares
A wrong timestamp is a wrong answer to three operational questions:
- Was the bug at 14:00 or 13:00? The investigator pivots on the chart at the wrong time. The hypothesis is wrong. The fix is delayed.
- Is the entry within retention? A timestamp one hour in the past can fall outside the per-tenant rejection window. The entry is silently dropped.
- Did the entries arrive in order? The ingester rejects out-of-order entries. A clock that runs backwards writes a second entry that is older than the first; the second entry is rejected at the ingester.
The alert that fires on the metric is the only signal the operator has. The push is silent. The query is silent. The metric is the signal.
How it works
Loki stamps each entry with the source timestamp extracted from the line. The pipeline is:
Source line Agent pipeline Loki distributor
+-----------------+ +-------------------+ +-------------------+
| 14:00:00 INFO | | parse -> map | | accept or reject |
| 14:00:01 ERROR | -> | -> extract ts | -> | -> stamp entry |
| 14:00:02 INFO | | -> rewrite | | -> store |
+-----------------+ +-------------------+ +-------------------+
Two boundaries fail at the wrong time:
- The agent to distributor boundary. The agent parses the source timestamp. If the parser fails, the agent sends the entry with no timestamp. The distributor stamps the entry with the ingestion time.
- The distributor to ingester boundary. The distributor
forwards the entry to the ingester. The ingester accepts
entries whose timestamp is newer than the stream’s last
timestamp and within the configured
rejection_older_thanwindow. Otherwise the entry is rejected with arejected_older_thanorout_of_orderreason.
Three wrong-time symptoms produce three operational shapes:
Host clock 200 ms behind true time
|
| source timestamp is 200 ms in the past
v
Agent stamps entry with parsed source timestamp
|
| pushes to distributor
v
Distributor accepts the entry
|
| forwards to ingester
v
Ingester compares against stream's last timestamp
|
| within rejection_older_than? within order?
| no no
v v v
reason="older_than" reason="out_of_order"
How to configure it
The collector configuration owns the timestamp. The pipeline stages must extract the source timestamp and either reject lines that fail to parse or forward them with the ingestion time. The minimal River configuration for RFC3339 source logs:
// /etc/alloy/config.alloy
loki.source.file "payments" {
targets = local.file_match("/var/log/payments/*.log")
forward_to = [loki.process.payments.receiver]
labels = { job = "payments", instance = sys.env("HOSTNAME") }
}
loki.process "payments" {
forward_to = [loki.write.default.receiver]
stage.match {
selector = "{job=\"payments\"}"
stage.regex {
expression = "^(?P<ts>\\S+)\\s+(?P<level>\\S+)\\s+(?P<msg>.*)$"
}
stage.timestamp {
source = "ts"
format = "RFC3339Nano"
fallback_format = "RFC3339"
action_on_error = "fwd"
}
}
}
loki.write "default" {
endpoint {
url = "http://loki-write.monitoring.svc:3100/loki/api/v1/push"
}
}
The three settings that change the failure mode are
stage.timestamp.format (the expected format), fallback_format
(an alternative to try when the first fails), and
action_on_error (what to do on a parse failure: fwd
forwards the line as raw text, drop drops the line,
no_match skips stages).
The Loki tenant-side limit controls how far in the past an entry can be accepted:
# /etc/loki/loki.yml
limits_config:
# Reject samples with timestamps older than 1 hour.
rejection_older_than: 1h
# Per-stream rate limit.
per_stream_rate_limit: 8MB
The retention window is the operational boundary. A source clock that runs behind real time by more than an hour will write entries that fall outside the window.
How to validate it
The validation reads three surfaces: the agent’s parse log, the distributor’s discarded-samples metric, and a representative query.
READ-ONLY: confirm the agent is parsing the timestamp.
sudo journalctl -u alloy -n 50 --no-pager | grep -i 'timestamp'
Expected output:
level=info msg="stage timestamp" path=/var/log/payments/payments.log
level=warn msg="failed to parse timestamp" path=/var/log/payments/payments.log
A failed to parse timestamp line means the agent saw a line
whose format was not the expected RFC3339. Count the lines; a
small number is normal (an old log file before the format
change); a large number is the source of the wrong-time problem.
READ-ONLY: confirm the distributor is rejecting entries for timestamp reasons.
curl -s http://loki-distributor.monitoring.svc:3100/metrics \
| grep -E 'loki_discarded_samples_total.*(older_than|out_of_order)'
Expected output:
loki_discarded_samples_total{reason="older_than",tenant="1"} 0
loki_discarded_samples_total{reason="out_of_order",tenant="1"} 0
A non-zero value here is the smoking gun. The reason label
identifies the failure shape: older_than is a clock behind
real time; out_of_order is a clock that ran backwards or a
single delayed batch.
READ-ONLY: confirm the stored entries are within the expected time.
logcli -addr http://loki-query.monitoring.svc:3100 query \
--since=1h '{job="payments"} | json | line_format "{{.ts}} {{.msg}}"'
Expected output:
2026-08-14T03:00:01.412Z payment failed for order 8812
2026-08-14T03:00:02.001Z payment failed for order 8813
2026-08-14T03:00:02.501Z payment failed for order 8814
The timestamps should be within the last hour. A line whose timestamp is more than an hour in the past is the wrong-time symptom; the entry should have been rejected by the agent.
READ-ONLY: confirm the ingester is accepting entries.
curl -s http://loki-ingester.monitoring.svc:3100/metrics \
| grep -E 'loki_ingester_streams_created_total'
loki_ingester_streams_created_total should be increasing. A
flat line plus a non-zero out_of_order counter means the
ingester is reading the entries but rejecting them.
How it can fail
Five specific failure shapes appear in production.
- The source clock is wrong. The host’s
dateis behind real time by one hour. The agent stamps the line with the host’s clock. The distributor accepts the entry; the ingester rejects it asolder_than. The symptom is a non-zeroloki_discarded_samples_total{reason="older_than"}. First hop:timedatectl statuson the source host. - The format string is wrong. The line is in RFC3339 but
the agent is configured to parse Unix epoch. The agent fails
to parse; the agent forwards the line with the ingestion
timestamp. The line is stored at the ingestion time, not the
source time. First hop: the agent’s
loki.processstage configuration. - The ingestion rate is normal, but the entry is rejected
as out-of-order. The host’s clock ran backwards (NTP step,
VM resume, container clock skew). The agent sends an entry
with a timestamp older than the previous entry in the stream.
The ingester rejects it as
out_of_order. First hop: the source host’s clock; theloki_ingester_received_entriesrate. - The timezone is implicit. The line is in local time
without an offset. The agent parses the wall-clock value but
cannot resolve it to UTC. The line is stored at the
local-clock interpretation. A team in
Europe/Berlinand a team inAmerica/New_Yorksee the same lines at different UTC offsets. First hop: the source log format; theformaton thestage.timestampblock. - The retention window is narrower than the source clock
skew. The per-tenant
rejection_older_thanis one hour; the source clock drifts by 90 seconds; the entry is accepted. The source clock then drifts by 95 minutes after a long outage; the entry is rejected asolder_than. First hop: the source host’s clock.
How to troubleshoot it
The diagnose-first order. Each step is read-only.
- Confirm the symptom. Reproduce the wrong-time symptom. The metric is the signal. The query is the symptom.
- Confirm the source clock.
timedatectl statuson the source host. TheSystem clock synchronizedline should beyes. TheNTP serviceshould beactive. - Confirm the agent parse log.
journalctl -u alloy -n 50 | grep -i timestamp. Thefailed to parse timestampline is the smoking gun. - Confirm the distributor discarded samples.
loki_discarded_samples_totalby reason. The reason label identifies the failure shape. - Confirm the ingester out-of-order counter.
loki_ingester_received_entriesand theloki_ingester_streamsgauge. A stream with a single flat timestamp is the symptom of one rejected entry among many. - Confirm the query returns the wrong-time entries.
logcli querywith--since=1h. The timestamps should be within the last hour. A line whose timestamp is days in the past is the wrong-time symptom.
The fix is to chase the source clock, the format string, or the time zone. The Loki side is rarely the cause.
Security implications
A wrong timestamp can hide an attack in two ways. The retention
window can be set so narrow that the entries from the attack
are dropped before they are stored. The clock skew can be so
deliberate that the operator cannot correlate the entries with
the rest of the trace. Treat a sudden, large increase in
loki_discarded_samples_total{reason="older_than"} as a
candidate for a security event. The metric is the only signal.
The Loki distributor accepts any push whose timestamp is within
the rejection_older_than window. The window is the trust
boundary. A narrow window is a small trust boundary; a wide
window is a large trust boundary. The choice is operational.
Performance implications
A wrong timestamp adds no cost to the chunk fetch. The wrong timestamp adds cost to the operator’s time. The cost is in the investigation that reaches the wrong conclusion because the chart at the wrong time told the wrong story.
The other performance trap is the rejection_older_than limit.
A wide window accepts more entries; the retention window later
deletes them. A narrow window rejects entries at the
distributor; the metric is non-zero. The choice is operational;
the trade-off is between the metric and the storage.
Production guidance
- Configure the agent to parse the source timestamp. The format string must match the format of the line.
- Set
action_on_error = "fwd"for production to avoid losing lines on a parser error. The cost is a non-zero “failed to parse” log; the benefit is no data loss. - Alert on
loki_discarded_samples_totalgrowth. The reason label identifies the failure shape. - Reject the source clock skew over widening the retention window. The window is the trust boundary; the clock is the agent’s responsibility.
- Run
chronyc trackingon every source host. The drift is the cause; the metric is the symptom.
Verification
You should now be able to answer:
- What is the difference between a source timestamp and an ingestion timestamp?
- Which Loki metric is the smoking gun for a timestamp that is older than the per-tenant window?
- Which Loki metric is the smoking gun for an out-of-order timestamp?
- What is the role of the agent stage.timestamp block in preventing wrong-time entries from reaching the ingester?
- Why is a wrong timestamp a worse symptom than a missing log line?
Quiz
Knowledge check · 8 questions
Q1. When a log line carries no parseable timestamp, Loki stamps the entry with:
Q2. A log line whose timestamp is older than the per-tenant rejection_older_than window is:
Q3. Loki 3.x accepts timestamps in RFC3339 with up to nanosecond precision.
Q4. Which of these are the two invariants the ingester validates per stream?
Q5. Name the Loki distributor metric that records the number of samples rejected because their timestamp is out of order.
Q6. A log line is timestamped in local time without an explicit offset. The most likely operational consequence is:
Q7. The right discipline when the rejection_older_than metric is non-zero is to:
Q8. The first hop when the agent logs a continuous stream of failed to parse timestamp warnings is:
Passing score: 75%. Answers are checked in this browser.