Daylight saving time bugs: rules for timestamps and reports
Practical rules to avoid daylight saving time bugs: store clean timestamps, show correct local time, and build reports users can verify and trust.

Why these bugs happen in normal products
Time bugs show up in normal products because people don’t live in UTC. They live in local time, and local time can jump forward, fall back, or change rules over the years. Two users can look at the same moment and see different clocks. Worse, the same local clock time can point to two different real moments.
Daylight saving time (DST) bugs often appear only twice a year, so they slip through. Everything looks fine in development, then a real customer books an appointment, files a timesheet, or checks a report on the switch weekend and something feels off.
Teams usually notice a few patterns first: a “missing hour” where scheduled items vanish or shift, a duplicated hour where logs or alerts look doubled, and daily totals that drift because a “day” was 23 or 25 hours.
This isn’t only a developer problem. Support gets tickets like “your app changed my meeting time.” Finance sees mismatched daily revenue. Ops wonders why overnight jobs ran twice or skipped. Even “created today” filters can disagree between users in different regions.
The goal is boring and reliable: store time in a way that never loses meaning, display local time the way humans expect, and build reports that stay true even on the weird days. When you do that, every part of the business can trust the numbers.
Whether you’re building with custom code or a platform like AppMaster, the rules are the same. You want timestamps that preserve the original moment, plus enough context (like the user’s time zone) to explain what that moment looked like on their clock.
A quick, plain-language model of time
Most DST bugs happen because we mix up “a moment in time” with “the way a clock shows it.” Keep those ideas separate and the rules get much simpler.
A few terms, in plain language:
- Timestamp: a precise moment on the timeline (independent of where you are).
- UTC: a global reference clock used to represent timestamps consistently.
- Local time: what a person sees on a wall clock in a place (like 9:00 AM in New York).
- Offset: the difference from UTC at a particular moment, written like +02:00 or -05:00.
- Time zone: a named set of rules that decides the offset for each date, like America/New_York.
An offset is not the same as a time zone. -05:00 only tells you the difference from UTC at one moment. It doesn’t tell you whether the place switches to -04:00 in summer, or if laws change next year. A time zone name does, because it carries the rules and history.
DST changes the offset, not the underlying timestamp. The event still happened at the same moment; only the local clock label changes.
Two hours cause most confusion:
- Spring skip: the clock jumps forward, so a range of local times never exists (for example, 2:30 AM can be impossible).
- Fall repeat: the clock repeats an hour, so the same local time happens twice (for example, 1:30 AM can be ambiguous).
If a support ticket is created at “1:30 AM” during the fall repeat, you need the time zone and the exact moment (UTC timestamp) to sort events correctly.
Data rules that prevent most problems
Most DST bugs start as a data problem, not a formatting problem. If the stored value is unclear, every screen and report later has to guess, and those guesses will disagree.
Rule 1: Store real events as an absolute moment (UTC)
If something happened at a specific moment (a payment captured, a ticket replied to, a shift started), store the timestamp in UTC. UTC doesn’t jump forward or backward, so it stays stable across DST changes.
Example: A support agent in New York replies at 9:15 AM local time on the day clocks change. Storing the UTC moment keeps that reply in the right order when someone in London reviews the thread later.
Rule 2: Keep time zone context as an IANA time zone ID
When you need to show the time in a human way, you need to know the user’s or location’s time zone. Store it as an IANA time zone ID like America/New_York or Europe/London, not as a vague label like “EST.” Abbreviations can mean different things, and offsets alone don’t capture DST rules.
A simple pattern is: event time in UTC, plus a separate time zone ID attached to the user, office, store, or device.
Rule 3: Store date-only values as dates, not timestamps
Some values aren’t moments in time. Birthdays, “renews on the 5th,” and “invoice due date” often should be stored as a date-only field. If you store them as a timestamp, time zone conversions can move them to the previous or next day.
Rule 4: Never store local time as a plain string without zone context
Avoid saving values like “2026-03-08 02:30” or “9:00 AM” without a time zone. That time might be ambiguous (happens twice) or impossible (skipped) during DST transitions.
If you must accept local input, store both the local value and the time zone ID, and convert to UTC for the actual event moment.
Deciding what to store for each kind of record
Many DST bugs happen because one record type gets treated like another. An audit log entry, a calendar meeting, and a payroll cutoff all look like “a date and time,” but they need different data to stay true.
For past events (things that already happened): store an exact moment, usually a UTC timestamp. If you ever need to explain it the way the user saw it, also store the user’s time zone at the time of the event (an IANA ID like America/New_York, not just “EST”). That lets you rebuild what the screen showed even if the user later changes their profile time zone.
For scheduling (things that should happen at a local wall-clock time): store the intended local date and time plus the time zone ID. Don’t convert it to UTC and throw away the original. “March 10 at 09:00 in Europe/Berlin” is the intent. UTC is a derived value that can change when rules change.
Changes are normal: people travel, offices relocate, companies update policies. For historical records, don’t rewrite past times when a user updates their profile time zone. For future schedules, decide whether the schedule follows the user (travel-friendly) or follows a fixed location (office-friendly), and store that location time zone.
Legacy data with only local timestamps is tricky. If you know the source time zone, attach it and treat the old time as local. If you don’t, mark it as “floating” and be honest in reports (for example, show the stored value without conversion). It also helps to model these as separate fields so screens and reports can’t accidentally mix them up.
Step-by-step: storing timestamps safely
To stop DST bugs, pick one unambiguous system of record for time, then only convert when you show it to people.
Write down the rule for your team: all timestamps in the database are UTC. Put it in docs and in code comments near date handling. This is the kind of decision that gets accidentally undone later.
A practical storage pattern looks like this:
- Pick UTC as the system of record and name fields to make it obvious (for example,
created_at_utc). - Add the fields you actually need: an event time in UTC (for example,
occurred_at_utc), and atz_idwhen local context matters (use an IANA time zone ID likeAmerica/New_York, not a fixed offset). - When accepting input, collect local date and time plus
tz_id, then convert to UTC once at the boundary (API or form submit). Don’t convert multiple times across layers. - Save and query in UTC. Convert to local time only at the edges (UI, emails, exports).
- For high-stakes actions (payments, compliance, scheduling), also log what you received (original local string,
tz_id, and the calculated UTC). That gives you an audit trail when users dispute a time.
Example: a user schedules “Nov 5, 9:00 AM” in America/Los_Angeles. You store occurred_at_utc = 2026-11-05T17:00:00Z and tz_id = America/Los_Angeles. Even if DST rules change later, you can still explain what they meant and what you stored.
If you’re modeling this in PostgreSQL (including via visual data modeling tools), make column types explicit and consistent, and enforce that your app writes UTC every time.
Displaying local time users can understand
Most DST bugs show up in the UI, not the database. People read what you show them, copy it into messages, and plan around it. If the screen is unclear, users will assume the wrong thing.
When time matters (bookings, tickets, appointments, delivery windows), show it like a receipt: complete, specific, and labeled.
Keep the display predictable:
- Show date + time + time zone (example: “Mar 10, 2026, 9:30 AM America/New_York”).
- Put the time zone label next to the time, not hidden in settings.
- If you show relative text (“in 2 hours”), keep the exact timestamp nearby.
- For shared items, consider showing both the viewer’s local time and the event’s time zone.
DST edge cases need explicit behavior. If you let users type any time, you’ll eventually accept a time that never happens or happens twice.
- Spring-forward (missing times): block invalid selections and offer the next valid time.
- Fall-back (ambiguous times): show the offset or a clear choice (for example, “1:30 AM UTC-4” vs “1:30 AM UTC-5”).
- Editing existing records: preserve the original instant even if formatting changes.
Example: A support agent in Berlin schedules a call with a customer in New York for “Nov 3, 1:30 AM.” During fall-back, that time occurs twice in New York. If the UI shows “Nov 3, 1:30 AM (UTC-4),” the confusion disappears.
Building reports that do not lie
Reports break trust when the same data gives different totals depending on where the viewer sits. To avoid DST bugs, decide what a report is actually grouping by, then stick to it.
First, pick the meaning of “day” for each report. A support team often thinks in the customer’s local day. Finance often needs the account’s legal time zone. Some technical reports are safest in UTC days.
Grouping by local day changes totals around DST. On the spring-forward day, one local hour is skipped. On the fall-back day, one local hour repeats. If you group events by “local date” without a clear rule, a busy hour can look missing, doubled, or moved to the wrong day.
A practical rule: every report has one reporting time zone, and it’s visible in the header (for example, “All dates shown in America/New_York”). That makes the math predictable and gives support something clear to point to.
For multi-region teams, it’s fine to let people switch the report time zone, but treat it as a different view of the same truth. Two viewers may see different daily buckets near midnight and DST transitions. That’s normal as long as the report clearly states the selected zone.
A few choices prevent most surprises:
- Define the report day boundary (user zone, account zone, or UTC) and document it.
- Use one time zone per report run, and show it next to the date range.
- For daily totals, group by local date in the chosen zone (not by UTC date).
- For hourly charts, label repeated hours on fall-back days.
- For durations, store and sum elapsed seconds, then format for display.
Durations need special care. A “2-hour shift” that crosses fall-back is 3 wall-clock hours but still 2 hours of elapsed time if the person worked 2 hours. Decide which meaning your users expect, then apply consistent rounding (for example, round after summing, not per row).
Common traps and how to avoid them
DST bugs aren’t “hard math.” They come from small assumptions that creep in over time.
A classic failure is saving a local timestamp but labeling it as UTC. Everything looks fine until someone in another time zone opens the record and it silently shifts. The safer rule is simple: store an instant (UTC) plus the right context (user or location time zone) when the record needs a local meaning.
Another frequent source of DST bugs is using fixed offsets like -05:00. Offsets don’t know about DST changes or historical rules. Use real IANA time zone IDs (like America/New_York) so your system can apply the correct rule for that date.
A few habits prevent many “double-shift” surprises:
- Convert only at the edges: parse input once, store once, display once.
- Keep a clear line between “instant” fields (UTC) and “wall clock” fields (local date/time).
- Store the time zone ID alongside records that depend on local interpretation.
- Make the server time zone irrelevant by always reading and writing UTC.
- For reports, define the report time zone and show it in the UI.
Also watch out for hidden conversions. A common pattern is: parse a user’s local time into UTC, then later a UI library assumes the value is local and converts again. The result is a one-hour jump that appears only for some users and some dates.
Finally, don’t use the client device time zone for billing or compliance. A traveler’s phone can change zones mid-trip. Instead, base those reports on an explicit business rule, such as the customer’s account time zone or a site location.
Testing: the few cases that catch most bugs
Most time bugs show up only a few days a year, which is why they slip through QA. The fix is testing the right moments and making those tests repeatable.
Pick one time zone that observes DST (for example, America/New_York or Europe/Berlin) and write tests for the two transition days. Then pick one zone that never uses DST (for example, Asia/Singapore or Africa/Nairobi) so you can see the difference clearly.
The 5 tests worth keeping forever
- Spring-forward day: verify the missing hour can’t be scheduled, and conversions don’t invent a time that never existed.
- Fall-back day: verify the duplicate hour, where two different UTC moments display as the same local time. Make sure logs and exports can distinguish them.
- Spans midnight: create an event that crosses midnight in local time, and confirm sorting and grouping still work when viewed in UTC.
- Non-DST contrast: repeat a conversion in a non-DST zone and confirm results stay stable across the same dates.
- Reporting snapshots: save expected totals for reports around month-end plus the DST weekend, and compare output after every change.
A concrete scenario
Imagine a support team schedules a “01:30” follow-up on fall-back night. If your UI stores only the displayed local time, you can’t tell which “01:30” they meant. A good test creates both UTC timestamps that map to 01:30 locally and confirms the app keeps them distinct.
These tests quickly show whether your system is storing the right facts (UTC instant, time zone ID, and sometimes the original local time) and whether reports stay honest when clocks change.
Quick checklist before you ship
Daylight saving time bugs slip through because the app looks right most days. Use this checklist before releasing anything that shows time, filters by date, or exports reports.
- Pick a single reporting time zone for each report (for example, “Business HQ time” or “User’s time”). Show it in the report header and keep it consistent across tables, totals, and charts.
- Store every “moment in time” as UTC (
created_at,paid_at,message_sent_at). Store the IANA time zone ID when you need context. - Don’t calculate with fixed offsets like “UTC-5” if DST can apply. Convert using the time zone rules for that date.
- Label times clearly everywhere (UI, emails, exports). Include date, time, and time zone so screenshots and CSVs aren’t misread.
- Keep a small DST test set: one timestamp just before the spring jump, one just after, and the same around the fall repeat hour.
A reality check: if a support manager in New York exports “Tickets created on Sunday” and a teammate in London opens the file, both should be able to tell what time zone the timestamps represent without guessing.
Example: a real support workflow across time zones
A customer in New York opens a support ticket during the week when the US has switched to daylight saving time, but the UK hasn’t yet. Your support team is in London.
On March 12, the customer submits the ticket at 09:30 local time in New York. That moment is 13:30 UTC, because New York is now UTC-4. A London agent replies at 14:10 London time, which is 14:10 UTC (London is still UTC+0 that week). The reply came 40 minutes after the ticket was created.
Here’s how this breaks if you store only local time with no time zone ID:
- You save “09:30” and “14:10” as plain timestamps.
- A report job later assumes “New York is always UTC-5” (or uses the server’s time zone).
- It converts 09:30 as 14:30 UTC, not 13:30 UTC.
- Your SLA clock is off by 1 hour, and a ticket that met a 2-hour SLA can be marked late.
The safer model keeps UI and reporting consistent. Store the event time as a UTC timestamp, and store the relevant IANA time zone ID (for example, America/New_York for the customer, Europe/London for the agent). In the UI, display the same UTC moment in the viewer’s time zone using the stored rules for that date.
For the weekly report, pick a clear rule like “group by customer local day.” Compute day boundaries in America/New_York (midnight to midnight), convert those boundaries to UTC, then count tickets inside them. The numbers stay stable, even during DST weeks.
Next steps: make time handling consistent across your app
If your product has been hit by DST bugs, the fastest way out is to write down a few rules and apply them everywhere. “Mostly consistent” is where time problems live.
Keep the rules short and specific:
- Storage format: what you store (usually an instant in UTC) and what you never store (ambiguous local time without a zone).
- Report time zone: which zone reports use by default, and how users can change it.
- UI labeling: what appears next to times (for example, “Mar 10, 09:00 (America/New_York)” vs just “09:00”).
- Rounding rules: how you bucket time (hour, day, week) and which zone those buckets follow.
- Audit fields: which timestamps mean “event occurred” vs “record created/updated.”
Roll it out in a low-risk way. Fix new records first so the problem stops growing. Then migrate historical data in batches. During migration, keep both the original value (if you have it) and the normalized value long enough to spot differences in reports.
If you’re using AppMaster (appmaster.io), one practical benefit is centralizing these rules in your data model and shared business logic: store UTC timestamps consistently, keep IANA time zone IDs alongside records that need local meaning, and apply conversions at input and display boundaries.
A practical next step is to build one time zone-safe report (like “tickets resolved per day”) and validate it using the test cases above. If it stays correct across a DST switch week for two different time zones, you’re in good shape.
FAQ
Daylight saving time changes the local clock offset, not the actual moment an event happened. If you treat a local clock reading like it’s the same as a real instant, you’ll see “missing” times in spring and “duplicate” times in fall.
Store real events as an absolute instant in UTC, so the value never jumps when offsets change. Then convert to a viewer’s local time only when displaying it, using a real time zone ID.
An offset like -05:00 only describes the difference from UTC at one moment and doesn’t include DST rules or history. An IANA time zone like America/New_York carries the full rule set, so conversions stay correct for different dates.
Store date-only things as dates when they are not real instants, like birthdays, invoice due dates, and “renews on the 5th.” If you store them as timestamps, converting between time zones can move them to the day before or after.
“Spring-forward” creates local times that never occur, so the app should block invalid selections and nudge to the next valid time. “Fall-back” creates a repeated hour, so the UI must let the user choose which instance they mean, typically by showing the offset.
For scheduling, store the intended local date and time plus the time zone ID, because that’s the user’s intent. You can also store a derived UTC instant for execution, but don’t throw away the original local intent or you’ll lose meaning when rules change.
Pick one reporting time zone per report and make it visible, so everyone knows what “day” means. Grouping by local day can produce 23-hour or 25-hour days near DST, which is fine as long as the report clearly states the chosen zone and applies it consistently.
Convert only at the boundaries: parse input once, store once, and format once for display. Double-conversion usually happens when one layer assumes a timestamp is local and another assumes it’s UTC, causing one-hour shifts that only appear on certain dates.
Store elapsed time in seconds (or another absolute unit) and sum those values, then format the result for display. Decide whether you mean elapsed time or wall-clock time before implementing payroll, SLAs, or shift lengths, because DST nights can make wall-clock hours look longer or shorter.
Test both DST transition days in at least one DST-observing zone and compare with a non-DST zone to spot assumptions. Include cases for the missing hour, the repeated hour, events near midnight, and report bucketing, because that’s where time bugs usually hide.


