Fix log correlation failures caused by timestamp and time zone drift
For developers debugging broken cross-system timelines in app, proxy, database, and host logs. This runbook gives you a fast decision path to identify whether the issue is clock drift, mixed time zones, bad parser assumptions, container host skew, or daylight-saving ambiguity, then apply the exact fix.
TL;DR — When log correlation breaks, the most common root cause is not your query tool; it is mixed timestamp standards across sources: one service emits local time, another emits UTC, and one host is drifting because NTP is unhealthy. Start by comparing
date,timedatectl, and a few raw log lines from each source; the fastest durable fix is to standardize all emitters to UTC in ISO-8601 with an explicit offset. Reading time: ~6 min
The scenario
You are halfway through a Tuesday deploy when alerts start firing for a spike in 500s. The reverse proxy shows the first errors at 14:03, the app logs say the same request failed at 09:03, and the database audit trail claims the write happened at 14:04:12+00. Your XDR timeline now shows the auth failure after the session creation, which is impossible, and the incident channel is already arguing about whether this is replay traffic or a bad release. You need to prove whether the systems are wrong, the parser is wrong, or the clocks are wrong.
Symptoms
- Requests that should line up across sources appear minutes or hours apart.
- Same request ID appears with impossible ordering, for example:
nginx: 203.0.113.10 - - [30/Oct/2026:14:03:11 +0000] "POST /login HTTP/1.1" 500 182 "-" "curl/8.7.1" req_id=7f2a
app: 2026-10-30 09:03:12,044 ERROR request_id=7f2a session lookup failed
postgres: 2026-10-30 14:03:12.221 UTC [8421] ERROR: relation "sessions" does not exist
- SIEM/XDR search windows miss obvious related events unless you widen the time range by hours.
- Authentication or API gateway logs show offsets while app logs do not, for example:
2026-10-30T14:03:11.991Z level=warn msg="token expired"
2026-10-30 14:03:12 level=info msg="refresh accepted"
journalctl, container logs, and file logs disagree on “now”.- Typical host signals:
$ timedatectl status
System clock synchronized: no
NTP service: active
RTC in local TZ: yes
Time zone: America/New_York (EDT, -0400)
- Typical parser/indexer signals when timestamps are misread:
failed to parse timestamp "2026-10-30 14:03:12" with layout RFC3339
using ingestion time because event timestamp is missing
@timestamp differs from source timestamp by 18034s
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| Mixed UTC/local time across services | Very common | `grep -R "log_format|datefmt|timezone|TZ|%z|%:z" /etc /app 2>/dev/null |
| Host clock drift or broken NTP/chrony | Very common | timedatectl status |
| Parser/indexer assumes wrong timestamp format or timezone | Common | `grep -E "parse timestamp |
Container time differs from expectation because host is wrong or TZ overrides display | Common | docker exec <container> date -Ins |
| DST ambiguity or local-time-only logs around clock change | Less common but nasty | `zdump -v /etc/localtime |
Step-by-step diagnosis
- Check whether the host clock is actually sane.
date -u -Ins && date -Ins && timedatectl status
If System clock synchronized: no, NTP service: inactive, or local and UTC times differ by something other than the expected offset, this is your problem. Jump to Fixes → Host clock drift or broken NTP/chrony.
- Compare raw timestamps from each source for the same request or event ID.
grep 'req_id=7f2a' /var/log/nginx/access.log /var/log/app.log /var/log/postgresql/postgresql*.log
If one source includes Z/+0000 and another emits bare local time like 2026-10-30 09:03:12, correlation will break depending on parser defaults. Jump to Fixes → Mixed UTC/local time across services.
- Verify what the process thinks its timezone is.
printf 'host=%s\n' "$(date +%Z,%z)"; docker exec app sh -lc 'printf "container=%s\n" "$(date +%Z,%z)"; env | grep ^TZ='
If the container prints a different zone than the host, or TZ is set in only some services, this is likely display-level inconsistency or an app runtime override. Jump to Fixes → Container time differs from expectation because host is wrong or TZ overrides display.
- Check whether your collector/parser is discarding the source timestamp and using ingestion time.
grep -R -E 'using ingestion time|failed to parse timestamp|timezone|RFC3339|strptime' /etc /var/log 2>/dev/null | head -50
If you see parse failures or fallback-to-ingest-time messages, this is your problem. Jump to Fixes → Parser/indexer assumes wrong timestamp format or timezone.
- Check for DST ambiguity if the incident is near a clock change and logs use local time without offset.
zdump -v /etc/localtime | tail -8
If the event time falls in the repeated hour during fall-back, e.g. 01:15 occurring twice, and your logs have no offset, you cannot reliably order events from raw timestamps alone. Jump to Fixes → DST ambiguity or local-time-only logs around clock change.
- Confirm the database and proxy are not using different timestamp conventions.
psql -Atqc "show timezone; select now(), now() at time zone 'utc';" && nginx -T 2>/dev/null | grep -n 'log_format\|access_log'
If Postgres says UTC but nginx/app logs are local time, or nginx format omits offset, treat it as mixed standards. Jump to Fixes → Mixed UTC/local time across services.
View as text
[Client] ---- 1. request req_id=7f2a ----> [Nginx] | | | v | 2. local time log | | v v [App] <---- 3. proxied request ---- [Collector] | ^ | 4. UTC log | v | [Postgres] ---- 5. UTC audit log -----------
Fixes
Mixed UTC/local time across services
Standardize emitters to UTC with explicit offset or Z. Do not rely on collector defaults.
For nginx, include an ISO-8601 timestamp with offset:
log_format main_json escape=json '{"ts":"$time_iso8601","remote_addr":"$remote_addr","request":"$request","status":$status,"req_id":"$request_id"}';
access_log /var/log/nginx/access.log main_json;
Reload:
nginx -t && systemctl reload nginx
For Python logging:
import logging, time
logging.Formatter.converter = time.gmtime
fmt = logging.Formatter('%(asctime)sZ %(levelname)s request_id=%(request_id)s %(message)s', '%Y-%m-%dT%H:%M:%S')
For Java/JVM:
JAVA_TOOL_OPTIONS="-Duser.timezone=UTC"
For Node.js, emit ISO strings explicitly:
console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'error', req_id, msg }))
For Postgres, pin timezone:
ALTER SYSTEM SET timezone = 'UTC';
SELECT pg_reload_conf();
SHOW timezone;
Verify it worked:
tail -n3 /var/log/nginx/access.log /var/log/app.log | sed 's/^/ /'
You want all sources to show Z or +0000 consistently.
Host clock drift or broken NTP/chrony
If the host clock is wrong, every process on it is suspect.
On systemd-timesyncd hosts:
timedatectl set-ntp true
systemctl restart systemd-timesyncd
systemctl status systemd-timesyncd --no-pager
On chrony hosts:
systemctl enable --now chronyd || systemctl enable --now chrony
chronyc tracking
chronyc sources -v
Typical bad output shape:
$ chronyc tracking
Reference ID : 00000000 ()
Stratum : 0
Leap status : Not synchronised
Good output should show a reference source and Leap status : Normal.
If the RTC is configured in local time, switch it to UTC:
timedatectl set-local-rtc 0
⚠️ Stepping the clock on a production database, message broker, or Kerberos/SSO node can cause auth failures, leader-election churn, or apparent time travel in logs. If offset is large, drain traffic or fail over first.
If offset is severe and you must correct immediately:
chronyc -a makestep
Verify it worked:
timedatectl status && chronyc tracking | sed -n '1,8p'
Parser/indexer assumes wrong timestamp format or timezone
Fix the parser to read the source timestamp, not ingestion time.
Examples of safe patterns:
{"ts":"2026-10-30T14:03:12.221Z","level":"error","req_id":"7f2a"}
2026-10-30T14:03:12+00:00 ERROR req_id=7f2a session lookup failed
If your collector uses strptime-style layouts, parse the exact format and set timezone only when the source lacks one:
timestamp:
field: ts
layouts:
- '2006-01-02T15:04:05.999Z07:00'
- '2006-01-02 15:04:05'
timezone: UTC
Trade-off: setting timezone: UTC on a parser is only correct for bare timestamps that are actually UTC. If some emitters send local time, you will silently shift data.
If your current pipeline falls back to ingestion time, disable that behavior or route parse failures to a dead-letter stream until fixed.
Verify it worked:
grep -E 'failed to parse timestamp|using ingestion time' /var/log/* 2>/dev/null | tail
No new parse failures should appear after rollout.
Container time differs from expectation because host is wrong or TZ overrides display
Containers share the host kernel clock. If the time is wrong inside the container, fix the host. If only the displayed zone differs, remove ad hoc TZ overrides and emit UTC explicitly.
Inspect:
docker inspect app --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^TZ='
Remove TZ from the service definition unless you have a hard requirement for local display:
services:
app:
environment:
- TZ=UTC
Or better, remove it entirely and format timestamps in UTC in the app.
For Kubernetes, check pod env and node time:
kubectl exec deploy/app -- date -Ins
kubectl get nodes -o wide
If one node is skewed, fix NTP on that node and reschedule pods if needed.
Verify it worked:
docker exec app date -u -Ins && docker exec app env | grep '^TZ=' || true
DST ambiguity or local-time-only logs around clock change
If logs say only 2026-11-01 01:15:00 with no offset, that timestamp may exist twice during fall-back. You cannot reconstruct exact order from those lines alone.
Immediate remediation:
- Re-query using adjacent sources that include offsets: proxy, load balancer, database, auth provider, or kernel/journal logs.
- Reconfigure the emitter to include offset or UTC.
For rsyslog, use RFC3339:
$template RFC3339Fmt,"%timegenerated:::date-rfc3339% %HOSTNAME% %syslogtag%%msg%\n"
*.* /var/log/messages;RFC3339Fmt
Reload:
systemctl restart rsyslog
For journald export, prefer UTC on read:
journalctl -o short-iso --utc -S '2026-11-01 00:30:00' -U '2026-11-01 02:30:00'
Verify it worked:
logger 'tz-test' && tail -n1 /var/log/messages
The new line should include an explicit offset or UTC-normalized timestamp.
Prevention
- Pin all service logs to ISO-8601 with offset in code and config. Add a CI grep that fails on bare local timestamps:
if grep -R -E '%Y-%m-%d %H:%M:%S|asctime|datefmt=.*%H:%M:%S[^%z]' .; then echo 'bare timestamp format found'; exit 1; fi
- Alert on clock sync health on every node:
timedatectl show -p NTPSynchronized -p TimeUSec -p RTCTimeUSec
chronyc tracking | grep -E 'Leap status|Last offset|RMS offset'
Create an alert if NTPSynchronized=no or offset exceeds your tolerance, e.g. >100ms for auth-heavy systems.
- Add an ingest pipeline check that rejects events without timezone info unless the source is explicitly documented as UTC:
{"rule":"timestamp must match /(Z|[+-][0-9]{2}:[0-9]{2}|[+-][0-9]{4})$/"}
- Store both source timestamp and collector receipt time. This lets you detect skew instead of hiding it:
{"ts":"2026-10-30T14:03:12.221Z","ingested_at":"2026-10-30T14:03:13.004Z"}
Alert when abs(ingested_at - ts) jumps unexpectedly.
- Add a post-deploy smoke test that writes one correlated event through proxy, app, and DB, then validates ordering:
req=$(uuidgen); curl -sS -H "X-Request-ID: $req" https://app.example.test/healthz >/dev/null; grep -R "$req" /var/log/nginx /var/log/app /var/log/postgresql
- For incident response, standardize query habits on UTC rendering:
export TZ=UTC
journalctl --utc -o short-iso -S -15m
psql -Atqc "select now() at time zone 'utc';"
This does not fix bad source data, but it prevents analysts from adding another layer of local-time confusion during triage.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI