Time Zone & time.Parse Gotchas
A report shows yesterday's date for events that happened today, but only for users in one timezone and only after DST.
This page covers time.Parse location rules, monotonic clock behavior, JSON serialization, and the debugging steps that surface time defects without guessing.
Summary
time.Time values represent an instant on the timeline plus an optional location for display.
Parsing mistakes - omitting zone in layout, mixing local and UTC, ignoring DST - produce subtle off-by-hours bugs that tests in one zone miss.
JSON APIs add another layer: default marshaling uses UTC RFC3339 while clients may interpret strings as local wall time.
Recipe
Quick-reference recipe card - copy-paste ready.
// Parse with explicit zone in layout
t, err := time.Parse(time.RFC3339, "2026-07-15T09:00:00-07:00")
// Parse local wall time in a specific zone
loc, _ := time.LoadLocation("America/Los_Angeles")
t, err := time.ParseInLocation("2006-01-02 15:04", "2026-07-15 09:00", loc)
// Store/compare in UTC, render in user zone
utc := t.UTC()
display := utc.In(loc)When to reach for this:
- Parsing CSV or log timestamps without offsets
- JSON APIs accepting date strings
- Scheduling jobs across DST boundaries
- Comparing
time.Now()with parsed historical times - Debugging "works in dev, wrong in prod" timezone skew
Working Example
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
At time.Time `json:"at"`
}
func main() {
// Zone-less parse -> UTC
t1, _ := time.Parse("2006-01-02 15:04:05", "2026-07-15 09:00:00")
fmt.Println("parsed no zone:", t1.Format(time.RFC3339), "loc", t1.Location())
loc, _ := time.LoadLocation("America/New_York")
t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-07-15 09:00:00", loc)
fmt.Println("parsed NY: ", t2.Format(time.RFC3339), "loc", t2.Location())
fmt.Println("equal instant?", t1.Equal(t2))
b, _ := json.Marshal(Event{At: t2})
fmt.Println("json:", string(b))
}What this demonstrates:
time.Parsewithout zone in the layout string assigns UTC for zone-less input- Same wall clock digits in different zones are different instants
Equalcompares instants;Formatoutput depends on location- JSON uses RFC3339 with offset or
Zfor UTC
Deep Dive
How It Works
- Reference time layout:
Mon Jan 2 15:04:05 MST 2006→ use2006-01-02style in layouts time.Parseuses UTC when layout has no zone;ParseInLocationsets intended zone- Monotonic clock:
time.Timemay include monotonic reading forSub/Since; stripped byFormatand JSON time.Now()returns local zone from environment unless you callUTC()
JSON and APIs
| Input | Typical bug | Fix |
|---|---|---|
"2026-07-15" date only | Parsed as UTC midnight | Document contract; use time.Date with zone |
| RFC3339 with offset | OK if layout includes offset | Use time.RFC3339 constant |
| Unix millis number | Zone-agnostic instant | Prefer for APIs crossing zones |
time.Time zero value | JSON "0001-01-01T00:00:00Z" | Use pointer or omit empty |
Go Notes
// Compare instants, not formatted strings
if !a.UTC().Equal(b.UTC()) { /* different moment */ }
// Truncate to calendar day in a zone
day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)Gotchas
- Parsing without zone in layout -
"2006-01-02 15:04:05"input becomes UTC, not local. Fix:ParseInLocationor require offset in input. - Comparing
Formatoutputs - Strings differ for same instant in different zones. Fix:Equalor compare UTC unix. - Assuming
time.Now().Location()is UTC in containers - Images withoutTZmay use UTC; laptops use local. Fix: setTZor alwaysUTC()in servers. - Missing tzdata in scratch images -
LoadLocationfails ondistrolesswithouttzdatapackage. Fix: embedtime/tzdataimport_ "time/tzdata". - DST gap and overlap - Local wall times can be ambiguous or nonexistent once per year. Fix: store UTC; convert for display only.
- Sub on times from different sources - Monotonic vs wall-only times mix in
Sub. Fix: uset.UTC()before arithmetic across parsed and clock times.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Store Unix seconds/millis in JSON | Global APIs, simple clients | Humans read raw JSON frequently |
time.RFC3339 strings | Standard HTTP/JSON interop | Need date-only without time |
civil date types (third-party) | Calendar scheduling without zone | Already committed to time.Time everywhere |
| Always UTC in database | Reporting consistency | Local midnight billing rules need zone table |
FAQs
Why does my parsed time show Z in JSON?
MarshalJSON on time.Time emits UTC RFC3339.
The instant is preserved; display zone is lost unless you add a separate field.
What layout string do I use?
Use the reference time components: year 2006, month 01, day 02, hour 15, minute 04, second 05, zone MST or -0700.
Copy constants like time.RFC3339 when they match input.
Does time.Parse respect the machine timezone?
Not for zone-less layouts - UTC is used.
Only ParseInLocation or offsets in the string set zone.
What is the monotonic clock?
An internal reading for measuring durations immune to wall clock jumps.
It does not appear in formatted output and is stripped when serializing.
How do I debug a one-hour offset?
Log t.Format(time.RFC3339), t.Location(), and t.UTC().Format(time.RFC3339).
Check whether input lacked offset and was parsed as UTC.
Why does LoadLocation fail in Docker?
Minimal images lack zoneinfo files.
Import _ "time/tzdata" or install tzdata in the image.
Should servers use UTC everywhere?
Store and compare in UTC; convert to user zone at UI boundary.
Document API contract if clients send local times.
How do databases interact with time.Time?
Drivers scan into time.Time with location often UTC.
Scanning into string loses type safety - prefer time.Time and explicit zone on write.
Are date-only strings safe?
time.Parse("2006-01-02", "2026-07-15") is UTC midnight.
Billing "local day" rules need ParseInLocation with business zone.
Does Add(24*time.Hour) cross DST correctly?
Add uses absolute duration on the timeline.
Calendar day arithmetic needs AddDate in the target location.
What about protobuf Timestamp?
timestamppb stores UTC instant.
Convert with AsTime() and .In(loc) for display.
How do I test timezone bugs?
Table-test with time.FixedZone and known offsets.
Set TZ in CI matrix or use LoadLocation with embedded tzdata.
Related
- Debugging Go: Observable Failures and Sharp Edges - time skew symptom family
- Go Debugging Basics - log RFC3339 and locations
- Struct Tags for JSON, DB, and Validation - time field tags and formats
- Data Types Best Practices - comparable value semantics
- Debugging & Defect Scenarios Best Practices - incident time logging rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).