I did this
Summary
curl --write-out '%time{%s}' (and %time{%s.%f}) outputs an epoch value that is
offset by the local timezone offset from UTC. On a host in Asia/Shanghai (UTC+8),
the value is consistently 28800 seconds (8 hours) too small.
Other %time{} specifiers such as %Y-%m-%d %H:%M:%S appear correct because they
read the broken-down UTC struct tm fields directly. Only %s is wrong.
The documentation states that %time{} uses UTC and that %s is
"The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC)."
Steps to reproduce
-
Set the system timezone to any non-UTC zone, e.g. Asia/Shanghai (UTC+8).
-
Run:
curl -s -o /dev/null -w 'epoch:%time{%s} date:%time{%Y-%m-%d %H:%M:%S}\n' https://example.com
date +%s
date -u '+%Y-%m-%d %H:%M:%S UTC'
- Compare
epoch: from curl with date +%s.
I expected the following
%time{%s} equals the real Unix epoch from gettimeofday() / date +%s.
%time{%s} is consistent with %time{%Y-%m-%d %H:%M:%S} for the same instant.
curl did this instead
Example output on Asia/Shanghai:
epoch:1781510611 date:2026-06-15 16:03:31
1781539411
2026-06-15 16:03:31 UTC
date +%s → 1781539411 (correct)
%time{%s} → 1781510611 (wrong, exactly 28800 seconds less)
%time{%Y-%m-%d %H:%M:%S} → 2026-06-15 16:03:31 (matches real UTC wall clock)
Decoding the wrong epoch:
$ date -u -d @1781510611
2026-06-15 08:03:31 UTC # 8 hours behind reality
$ date -u -d @1781539411
2026-06-15 16:03:31 UTC # correct
The date line shows the correct UTC time, but %s does not match that instant.
Root cause
In src/tool_writeout.c, function outtime():
- Reads wall time with
gettimeofday() → secs (correct epoch).
- Converts to UTC with
curlx_gmtime(secs, &utc).
- Passes user format (including
%s) to strftime(..., &utc).
%f, %z, and %Z are already expanded manually before strftime().
%s is not, so glibc's strftime() handles it.
On glibc, strftime() with %s calls mktime() on the supplied struct tm,
treating it as local time, not UTC. Because utc holds UTC components,
the result is the epoch for "those wall-clock numbers interpreted as local time",
which is offset by the timezone.
This is the same pattern already avoided for %f / %z / %Z.
Minimal C reproducer (glibc strftime behaviour)
Save as strftime_s_gmtime.c and compile with gcc -o strftime_s_gmtime strftime_s_gmtime.c.
strftime_s_gmtime(reproducer).c
I expected the following
I expected the following
curl -s -o /dev/null -w 'epoch:%time{%s} date:%time{%Y-%m-%d %H:%M:%S}\n' https://example.com
date +%s
date -u '+%Y-%m-%d %H:%M:%S UTC'
%time{%s} equals the real Unix epoch from gettimeofday() / date +%s.
%time{%s} is consistent with %time{%Y-%m-%d %H:%M:%S} for the same instant.
Suggested fix
Handle %s the same way %f is handled: expand it to the numeric value of
secs from gettimeofday() before calling strftime(). Use lowercase
s only; do not touch %S (seconds field).
patch:
fix-time-s.patch
After this patch, on Asia/Shanghai:
$ curl -s -o /dev/null -w '%time{%s}\n' https://example.com
1781576611
$ date +%s
1781576611
Additional notes
- Affects any host where
localtime offset from UTC is non-zero.
- Not specific to snap vs source builds; reproducible with vanilla curl 8.20.0.
%time{%s.%f} is also fixed because both %s and %f are expanded manually.
- Using two separate
%time{} variables may still differ in the microsecond
fraction because the clock is read once per %time{} invocation.
References
curl/libcurl version
curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.0.2 ...
operating system
- OS: Ubuntu 22.04 / Ubuntu Kylin 22.04 (jammy), x86_64
- libc: glibc 2.35
- Timezone:
Asia/Shanghai (CST, +0800)
TZ unset (system default)
I did this
Summary
curl --write-out '%time{%s}'(and%time{%s.%f}) outputs an epoch value that isoffset by the local timezone offset from UTC. On a host in
Asia/Shanghai(UTC+8),the value is consistently 28800 seconds (8 hours) too small.
Other
%time{}specifiers such as%Y-%m-%d %H:%M:%Sappear correct because theyread the broken-down UTC
struct tmfields directly. Only%sis wrong.The documentation states that
%time{}uses UTC and that%sis"The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC)."
Steps to reproduce
Set the system timezone to any non-UTC zone, e.g.
Asia/Shanghai(UTC+8).Run:
epoch:from curl withdate +%s.I expected the following
%time{%s}equals the real Unix epoch fromgettimeofday()/date +%s.%time{%s}is consistent with%time{%Y-%m-%d %H:%M:%S}for the same instant.curl did this instead
Example output on
Asia/Shanghai:date +%s→1781539411(correct)%time{%s}→1781510611(wrong, exactly 28800 seconds less)%time{%Y-%m-%d %H:%M:%S}→2026-06-15 16:03:31(matches real UTC wall clock)Decoding the wrong epoch:
The date line shows the correct UTC time, but
%sdoes not match that instant.Root cause
In
src/tool_writeout.c, functionouttime():gettimeofday()→secs(correct epoch).curlx_gmtime(secs, &utc).%s) tostrftime(..., &utc).%f,%z, and%Zare already expanded manually beforestrftime().%sis not, so glibc'sstrftime()handles it.On glibc,
strftime()with%scallsmktime()on the suppliedstruct tm,treating it as local time, not UTC. Because
utcholds UTC components,the result is the epoch for "those wall-clock numbers interpreted as local time",
which is offset by the timezone.
This is the same pattern already avoided for
%f/%z/%Z.Minimal C reproducer (glibc strftime behaviour)
Save as
strftime_s_gmtime.cand compile withgcc -o strftime_s_gmtime strftime_s_gmtime.c.strftime_s_gmtime(reproducer).c
I expected the following
I expected the following
%time{%s}equals the real Unix epoch fromgettimeofday()/date +%s.%time{%s}is consistent with%time{%Y-%m-%d %H:%M:%S}for the same instant.Suggested fix
Handle
%sthe same way%fis handled: expand it to the numeric value ofsecsfromgettimeofday()before callingstrftime(). Use lowercasesonly; do not touch%S(seconds field).patch:
fix-time-s.patch
After this patch, on
Asia/Shanghai:$ curl -s -o /dev/null -w '%time{%s}\n' https://example.com 1781576611 $ date +%s 1781576611Additional notes
localtimeoffset from UTC is non-zero.%time{%s.%f}is also fixed because both%sand%fare expanded manually.%time{}variables may still differ in the microsecondfraction because the clock is read once per
%time{}invocation.References
%time{}added in curl 8.16.0: writeout: add %time{} #18119outtime()insrc/tool_writeout.ccurl/libcurl version
curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.0.2 ...
operating system
Asia/Shanghai(CST, +0800)TZunset (system default)