UseToolSuite UseToolSuite
Time & Date 📖 Pillar Guide

Time & Date in Programming: The Complete Developer Guide

A developer's guide to handling time: the Unix epoch and Year 2038, IANA timezones and DST, JavaScript Date traps, ISO 8601, cron scheduling, TIMESTAMPTZ vs DATETIME, forcing TZ=UTC, and a pre-deploy checklist.

Necmeddin Cunedioglu Necmeddin Cunedioglu 10 min read

Practice what you learn

Timestamp & Timezone Converter

Try it free →

Time handling is one of the most bug-prone areas in software, and it’s deceptively complex.

Consider a Node.js cron script that runs a report “every morning at 8:00 AM.” It deploys fine and works for six months. Then one Sunday in November it runs twice and double-charges a customer. In March it doesn’t run at all. Users in London get the report an hour late, and users in Tokyo get the wrong day’s data.

The root cause: human time is a political construct, not a mathematical constant.

Timezone offsets shift with Daylight Saving Time. Governments change their timezone rules with little warning. Leap seconds get inserted to account for the Earth’s slowing rotation. In 2011, Samoa skipped Friday, December 30th entirely — jumping from Thursday to Saturday — to align its trading week with Australia.

To work in backend APIs, distributed databases, or frontend localization, you need to understand how systems actually track time. This guide covers the Unix epoch, the Year 2038 problem, the IANA timezone database, ISO 8601, and scheduling with cron.

Stop doing timezone math in your head. Convert epochs, inspect ISO 8601 strings, and calculate offsets with our local Unix Timestamp Converter.

1. The Unix Epoch

Before you format a date for a user, you need to know how the system stores it. Almost all modern systems (Linux servers, iOS phones) track time with the Unix timestamp (also called POSIX time or the Unix epoch).

A Unix timestamp is simple: it’s an integer counting the seconds elapsed since January 1, 1970, 00:00:00 UTC.

  • It has no timezone.
  • It ignores Daylight Saving Time.
  • It only increases.

The seconds vs milliseconds trap

The most common timestamp bug in distributed systems is a unit mismatch. Languages default to different units for the epoch.

LanguageFunctionUnitDigits
JavaScript / Node.jsDate.now()Milliseconds13
JavaSystem.currentTimeMillis()Milliseconds13
Pythontime.time()Seconds (float)10 (before the decimal)
PHPtime()Seconds10
Gotime.Now().Unix()Seconds10
RubyTime.now.to_iSeconds10
C / C++time(NULL)Seconds10

The bug: if a Python backend sends 1711324800 (10 digits, March 2024) to a React frontend and JavaScript parses it as milliseconds with new Date(1711324800), the user sees January 20, 1970.

The fix: document your API contracts. When passing a 10-digit second-based timestamp into JavaScript, multiply by 1,000 first: new Date(timestamp * 1000).

Negative timestamps and the Year 2038 problem

Because the epoch starts in 1970, earlier dates are negative integers.

  • 0 → January 1, 1970 00:00:00 UTC
  • -86400 → December 31, 1969 00:00:00 UTC

More importantly, many legacy systems (especially embedded hardware) store the timestamp as a 32-bit signed integer, which maxes out at 2,147,483,647.

On January 19, 2038 at 03:14:07 UTC, that integer overflows. Systems using 32-bit storage roll over to a large negative number and read the date as December 13, 1901.

This is the Y2K38 problem (the “Epochalypse”). It will cause failures in legacy databases, IoT devices, embedded firmware, and 32-bit routers.

The fix: use 64-bit integers (BIGINT in PostgreSQL/MySQL) for timestamp columns. A 64-bit integer pushes the overflow billions of years out.

2. Timezones

Timezones aren’t fixed offsets — they’re geopolitical regions that change with politics.

Before anything else, one distinction that removes a surprising amount of confusion: UTC is not a timezone. It is the reference point that timezones are defined against — a standard, not a place, and it never observes daylight saving. Nobody’s wall clock is “in UTC” by geography; servers are set to it precisely because it is the one offset that never moves.

The IANA timezone database (tzdata)

Never use three-letter abbreviations like EST or CST in your code.

  • EST can mean UTC-5 (New York) but is also Eastern Standard Time in Australia (UTC+10).
  • New York is only EST for half the year — in summer it’s EDT (UTC-4).
  • Hardcoding -05:00 is just as wrong, since the offset changes twice a year.

The safe way to handle timezones is with IANA timezone identifiers (America/New_York, Europe/London, Asia/Tokyo). These map to the constantly-updated open-source tzdata database, which tracks every historical, current, and planned offset change for each region.

Daylight Saving Time anomalies

DST advances clocks by an hour in warmer months, and it breaks naive time arithmetic.

  1. The missing hour (spring forward): when clocks jump from 2:00 AM to 3:00 AM, the 2:00–2:59 AM hour doesn’t exist that day. Schedule something for 2:30 AM and your code either errors or silently shifts to 3:30 AM.
  2. The doubled hour (fall back): when clocks fall from 3:00 AM to 2:00 AM, the 2:00–2:59 AM hour happens twice. Group analytics by hour and that hour holds twice the data, skewing your metrics.

The solution: never store or compute in local time. All backend logic, databases, and cron schedules should run in UTC. UTC doesn’t observe DST — it’s predictable. Convert to the user’s local timezone only at the last moment, during UI rendering.

3. JavaScript’s Date Traps

Date was written in ten days in 1995 and inherited its worst decisions from Java. Two of them still generate bug reports every week.

Trap 1: the constructor silently uses local time

// ❌ timezone-dependent — the same line produces different instants
new Date('2026-03-15T00:00:00')
//   in New York  -> midnight EST  (05:00 UTC)
//   in London    -> midnight GMT  (00:00 UTC)

// ✅ the Z pins it to a real instant everywhere
new Date('2026-03-15T00:00:00Z')

A date-only string ('2026-03-15') is parsed as UTC, while a date-time string without a zone is parsed as local. That inconsistency is in the spec, and it is why a test suite can pass on a developer’s laptop and fail in CI.

Trap 2: months are 0-indexed, days are not

const d = new Date('2026-03-15T00:00:00Z');
d.getMonth(); // 2  — March
d.getDate();  // 15 — days are 1-indexed

new Date(Date.UTC(2026, 12, 25)); // ❌ 25 Jan 2027 — month 12 rolls over
new Date(Date.UTC(2026, 11, 25)); // ✅ 25 Dec 2026

The overflow is silent: month 12 does not throw, it rolls into the next year.

The fix: stop formatting dates by hand

Intl.DateTimeFormat does localisation, translation and zone conversion in one call, and it reads from the same tzdata as everything else:

const date = new Date('2026-03-15T14:30:00Z');

new Intl.DateTimeFormat('ja-JP', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'full',
  timeStyle: 'long',
}).format(date);
// "2026年3月15日日曜日 23時30分00秒 日本標準時"

Every string-concatenation date formatter eventually grows a timezone bug. This one cannot, because it never sees a local time.

4. ISO 8601 Formatting

When sending dates over JSON or GraphQL, use an unambiguous, machine-readable format. The standard is ISO 8601.

TypeExampleUse case
Date only2026-03-17Birthdays, card expirations, billing cycles — when time of day doesn’t matter.
DateTime (UTC)2026-03-17T14:30:00.000ZThe default. Use for API payloads and DB storage. The Z means Zulu time (UTC).
DateTime (offset)2026-03-17T09:30:00-05:00When you need the user’s wall-clock time and their offset.
DurationP1Y2M3DT4H5M6SIntervals (1 year, 2 months, 3 days, 4 hours, 5 min, 6 sec).

Formatting for users (the Intl API)

Frontend developers used to import 300KB+ libraries like Moment.js just to format dates. Moment.js is now deprecated.

Don’t concatenate strings (month + "/" + day + "/" + year) — different countries expect different formats (MM/DD/YYYY in the US, DD/MM/YYYY in the EU). Use the native Intl (Internationalization) API, which is built into modern browsers and Node.

const rawDatabaseTimestamp = "2026-03-17T14:30:00.000Z"; // Stored in UTC

// The browser translates the UTC string to the user's language and timezone
const formattedUIString = new Intl.DateTimeFormat('fr-FR', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'Europe/Paris' // Force the Paris timezone
}).format(new Date(rawDatabaseTimestamp));

console.log(formattedUIString); 
// Output: "mardi 17 mars 2026 à 15:30"

5. Scheduling with Cron

Cron is the Unix daemon for scheduling recurring tasks. It uses a 5-field, space-delimited syntax.

┌───────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌───────── month (1-12)
│ │ │ │ ┌───────── day of week (0-7, where 0 and 7 are Sunday)
│ │ │ │ │
* * * * *

Common patterns

  • * * * * * — every minute.
  • */15 * * * * — every 15 minutes (/ is a step interval).
  • 0 * * * * — at the top of every hour.
  • 0 0 * * * — every day at midnight.
  • 0 9 * * 1-5 — 9:00 AM, Monday through Friday (1-5 is a range).

Cron and local timezones

By default, the cron daemon evaluates expressions against the server’s system clock. If your EC2 instance or dev machine is set to a local timezone (America/New_York), your cron jobs are exposed to the DST anomalies above.

Schedule a backup with 30 2 * * * (2:30 AM) and it won’t run on the spring-forward day in March, because 2:30 AM doesn’t exist.

The fix: set all your infrastructure to UTC (sudo timedatectl set-timezone UTC) and write your cron schedules in UTC.

Validate before it hits production: a typo in a cron expression can run a heavy query every minute instead of every month. Paste your expression into our Cron Expression Parser to read it in plain English and see the next 5 run times.

6. The Exception to “Always Store UTC”

There is exactly one case where converting to UTC up front is wrong: a future event tied to a human’s wall clock.

A user sets a daily alarm for “07:00 in New York.” Resolve that to a UTC instant today and store it, and the alarm is correct until the next DST transition — after which it rings at 06:00 or 08:00 forever. Worse, governments keep changing the rules; every proposal to abolish DST would silently break every UTC-anchored recurring schedule in your database.

Store the intent, not the resolved instant: the wall-clock time (07:00:00) and the IANA zone (America/New_York) in separate columns, resolved to UTC at fire time against the current tzdata. A past event is an instant and belongs in UTC; a future local-time commitment is a rule and has to stay one.

7. Storing Dates in SQL

The database is where a timezone bug stops being a display glitch and becomes permanent data corruption. Use the column types built for temporal data — never VARCHAR.

PostgreSQL

Use TIMESTAMPTZ. The name is misleading: Postgres does not store a zone. It converts the incoming value to UTC, stores it as an 8-byte integer, and converts back to the session’s zone on SELECT.

CREATE TABLE events (
  id         SERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Pin API responses to UTC regardless of the session default
SET TIME ZONE 'UTC';
SELECT created_at FROM events;

Avoid plain TIMESTAMP (without time zone). It stores the wall-clock string blindly, so the moment your server moves regions every stored value becomes unfalsifiably ambiguous.

MySQL / MariaDB

Two types, and the trade-off is real:

  1. TIMESTAMP converts from the connection’s zone to UTC on write and back on read — but it is 32-bit and dies at 2038-01-19 03:14:07 UTC, the same overflow from section 1.
  2. DATETIME stores exactly what you hand it with no translation. Future-proof, but you must guarantee UTC on the way in.
-- Be explicit rather than relying on the connection's timezone
INSERT INTO events (created_at) VALUES (UTC_TIMESTAMP());

-- Convert on read (requires the tz tables to be populated)
SELECT CONVERT_TZ(created_at, 'UTC', 'America/New_York') AS local_time FROM events;

8. Server and Runtime Configuration

Most “works on my machine” time bugs are a laptop in one zone and a production host in another. Remove the variable everywhere:

# Host
sudo timedatectl set-timezone UTC
date   # should end in UTC

# Per-process, for containers and CI runners
TZ=UTC npm start
TZ=UTC python3 app.py

Node and Python inherit the system zone unless TZ says otherwise, so setting it explicitly in your Dockerfile and CI config is cheaper than diagnosing the one test that fails for six months of the year.

The Pre-Deploy Checklist

  • Timestamps stored in UTC, or in a UTC-aware type (TIMESTAMPTZ).
  • API responses use ISO 8601 with an explicit Z.
  • User preferences store IANA names (Europe/Paris), never abbreviations (CET).
  • Display goes through Intl.DateTimeFormat or date-fns-tz, not string concatenation.
  • Containers, VMs and CI runners all run TZ=UTC.
  • Cron jobs run in UTC and tolerate the missing and doubled DST hours.
  • Future local-time events stored as wall-clock time + IANA zone, not a resolved UTC instant.

Further Reading


Handle temporal programming with confidence. Convert epochs and validate ISO strings with our Timestamp Converter, and debug recurring schedules with the visual Cron Parser.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
• 10 min read •
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.