Working with Timestamps and Dates in Web Development

Time and date handling is one of the most challenging aspects of web development. Between Unix timestamps, ISO 8601 formats, timezones, and daylight saving time, developers face numerous pitfalls. This guide will help you master date and time handling across different platforms and use cases.

Understanding Unix Timestamps

What is a Unix Timestamp?

A Unix timestamp represents the number of seconds (or milliseconds) that have elapsed since January 1, 1970, 00:00:00 UTC (the Unix epoch). This standardized format makes it easy to compare, store, and transmit time data.

// Current timestamp in seconds
const timestampSeconds = Math.floor(Date.now() / 1000);
// 1705075200

// Current timestamp in milliseconds
const timestampMs = Date.now();
// 1705075200000

Why Use Unix Timestamps?

Advantages:

  • Universal: Works across all programming languages and databases
  • No timezone confusion: Always stored in UTC
  • Easy math: Simple to calculate differences and add time
  • Compact: Efficient storage as a single integer
  • Sortable: Natural ordering for time-series data

Use cases:

  • API responses and requests
  • Database storage for created_at/updated_at fields
  • Log file timestamps
  • Session expiration times
  • Cache expiration

Date Format Standards

ISO 8601: The Universal Standard

ISO 8601 is the international standard for date and time representation. It's human-readable and machine-parseable.

Formats:

2025-01-12T14:30:00Z           // UTC time
2025-01-12T14:30:00+08:00      // With timezone offset
2025-01-12T14:30:00.123Z       // With milliseconds
2025-01-12                      // Date only
14:30:00                        // Time only

Why use ISO 8601:

  • Unambiguous date representation
  • Sorts correctly as strings
  • Supported by all modern languages
  • Avoids MM/DD vs DD/MM confusion

RFC 3339

RFC 3339 is a profile of ISO 8601 commonly used in internet protocols:

2025-01-12T14:30:00Z
2025-01-12T14:30:00+08:00

Use cases:

  • HTTP headers (Date, Last-Modified)
  • JSON API responses
  • OAuth timestamps
  • RSS/Atom feeds

Language-Specific Formats

Different languages have their own date formatting conventions:

// JavaScript
const date = new Date('2025-01-12T14:30:00Z');
date.toISOString();        // "2025-01-12T14:30:00.000Z"
date.toUTCString();        // "Sun, 12 Jan 2025 14:30:00 GMT"
date.toLocaleDateString(); // "1/12/2025" (locale dependent)

// Python
from datetime import datetime
dt = datetime(2025, 1, 12, 14, 30, 0)
dt.isoformat()            # "2025-01-12T14:30:00"
dt.strftime("%Y-%m-%d")   # "2025-01-12"
dt.strftime("%B %d, %Y")  # "January 12, 2025"

Timezone Handling

The Timezone Problem

Timezones are complex due to:

  • UTC offsets: Range from UTC-12 to UTC+14
  • Daylight Saving Time (DST): Changes twice per year in many regions
  • Political changes: Countries occasionally change their timezone rules
  • Historical data: Different rules applied in the past

Best Practices

1. Always store times in UTC

-- Good: Store in UTC
CREATE TABLE events (
  id INT PRIMARY KEY,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  -- Stored as UTC internally
);

-- Bad: Storing in local time
-- Don't do this!

2. Convert to user's timezone only for display

// Store in UTC
const utcDate = new Date();

// Display in user's timezone
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit'
});

console.log(formatter.format(utcDate));
// "January 12, 2025 at 09:30 AM"

3. Use timezone-aware libraries

Instead of manual timezone calculations, use battle-tested libraries:

  • JavaScript: Luxon, date-fns-tz, Moment.js (deprecated but still widely used)
  • Python: pytz, python-dateutil, zoneinfo (Python 3.9+)
  • Java: java.time.ZonedDateTime
  • Ruby: ActiveSupport::TimeZone
// Using Luxon
import { DateTime } from 'luxon';

const dt = DateTime.fromISO('2025-01-12T14:30:00Z');
const tokyo = dt.setZone('Asia/Tokyo');
const newYork = dt.setZone('America/New_York');

console.log(tokyo.toFormat('HH:mm'));    // "23:30"
console.log(newYork.toFormat('HH:mm'));  // "09:30"

Common Pitfalls and Solutions

Pitfall 1: Mixing Seconds and Milliseconds

// JavaScript uses milliseconds
Date.now();              // 1705075200000

// Unix timestamps are usually in seconds
Math.floor(Date.now() / 1000);  // 1705075200

// Detecting the format
function parseTimestamp(ts) {
  // If < 10^10, likely seconds; otherwise milliseconds
  return ts < 10000000000
    ? new Date(ts * 1000)
    : new Date(ts);
}

Pitfall 2: Assuming Date Parsing is Consistent

// Inconsistent across browsers
new Date('2025-01-12');           // May be UTC or local
new Date('2025-01-12T00:00:00');  // Treated as local time
new Date('2025-01-12T00:00:00Z'); // Always UTC ✓

// Best practice: Always be explicit
new Date('2025-01-12T00:00:00.000Z'); // ✓

Pitfall 3: Not Accounting for Leap Seconds

While rare, leap seconds do exist. Use reliable time libraries that handle them:

// Most systems ignore leap seconds
// Use NTP-synchronized time sources for accuracy

Pitfall 4: Hardcoding Timezone Offsets

// Bad: Hardcoded offset
const offset = 8 * 60 * 60 * 1000; // 8 hours
const localTime = new Date(utcTime.getTime() + offset);

// Good: Use proper timezone handling
const localTime = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Shanghai'
}).format(utcTime);

Database Considerations

PostgreSQL

-- TIMESTAMP stores without timezone (local)
CREATE TABLE events (
  created_at TIMESTAMP
);

-- TIMESTAMPTZ stores with timezone (recommended)
CREATE TABLE events (
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Converting
SELECT created_at AT TIME ZONE 'UTC' AS utc_time
FROM events;

MySQL

-- DATETIME stores as-is (no timezone)
CREATE TABLE events (
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- TIMESTAMP converts to UTC internally
CREATE TABLE events (
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

MongoDB

// Stores as milliseconds since epoch
db.events.insert({
  created_at: new Date()  // ISODate("2025-01-12T14:30:00.000Z")
});

// Queries work with Date objects
db.events.find({
  created_at: {
    $gte: new Date('2025-01-01'),
    $lt: new Date('2025-02-01')
  }
});

Practical Examples

Example 1: Building a Countdown Timer

function createCountdown(targetTimestamp) {
  const target = new Date(targetTimestamp);

  const update = () => {
    const now = Date.now();
    const diff = target.getTime() - now;

    if (diff <= 0) {
      return { expired: true };
    }

    const days = Math.floor(diff / (1000 * 60 * 60 * 24));
    const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
    const seconds = Math.floor((diff % (1000 * 60)) / 1000);

    return { days, hours, minutes, seconds, expired: false };
  };

  return update;
}

// Usage
const countdown = createCountdown('2025-12-31T23:59:59Z');
setInterval(() => {
  const time = countdown();
  if (time.expired) {
    console.log('Happy New Year!');
  } else {
    console.log(`${time.days}d ${time.hours}h ${time.minutes}m ${time.seconds}s`);
  }
}, 1000);

Example 2: Scheduling Jobs with Cron

// Converting from cron expression to next run time
function getNextCronRun(cronExpression, timezone = 'UTC') {
  // Using a library like node-cron or cron-parser
  const parser = require('cron-parser');

  const interval = parser.parseExpression(cronExpression, {
    tz: timezone
  });

  return interval.next().toDate();
}

// Run every day at 2 AM UTC
const nextRun = getNextCronRun('0 2 * * *', 'UTC');
console.log(nextRun.toISOString());

Example 3: Relative Time Display

function relativeTime(timestamp) {
  const now = Date.now();
  const diff = now - new Date(timestamp).getTime();

  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (seconds < 60) return 'just now';
  if (minutes < 60) return `${minutes}m ago`;
  if (hours < 24) return `${hours}h ago`;
  if (days < 7) return `${days}d ago`;

  // Fall back to formatted date
  return new Date(timestamp).toLocaleDateString();
}

console.log(relativeTime('2025-01-12T14:00:00Z'));
// "2h ago" or "just now" depending on current time

Testing Time-Dependent Code

Mock Current Time

// Using Jest
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-12T00:00:00Z'));

// Your code that uses Date.now() or new Date()
const result = scheduleTask();

// Fast-forward time
jest.advanceTimersByTime(1000 * 60 * 60); // 1 hour

// Clean up
jest.useRealTimers();

Time-Travel Testing

// Test DST transitions
const testCases = [
  '2025-03-09T01:59:59-05:00', // Before DST
  '2025-03-09T03:00:00-04:00', // After DST (2 AM doesn't exist!)
];

testCases.forEach(timestamp => {
  const result = processEvent(timestamp);
  expect(result).toBeDefined();
});

Tools for Working with Dates

Use toolcli Time Studio to:

  • Convert between timestamps and human-readable formats
  • Parse various date formats (ISO 8601, RFC 3339, Excel serial)
  • Extract timestamps from MongoDB ObjectIDs
  • Visualize timezone conversions

Conclusion

Working with timestamps and dates requires careful attention to:

  1. Always store in UTC, convert for display
  2. Use standard formats (ISO 8601, RFC 3339)
  3. Leverage libraries instead of manual calculations
  4. Test edge cases (DST transitions, leap years, epoch boundaries)
  5. Be explicit about timezones and formats

By following these practices, you'll avoid common pitfalls and build robust time-handling code that works correctly across different timezones and locales.

Additional Resources