The Internal Representation: time_t and 32-bit vs 64-bit

The kernel stores the current time as a signed integer in a C type called `time_t`. On 32-bit systems, `time_t` is a signed 32-bit integer, which overflows on January 19, 2038 at 03:14:07 UTC - the so-called Year 2038 problem. That timestamp in epoch seconds is 2,147,483,647, the maximum value of a signed 32-bit int.

On 64-bit Linux systems, `time_t` is a signed 64-bit integer, giving you a range that extends roughly 292 billion years in either direction. Check what your system is using:

```c #include #include int main() { printf("%zu\n", sizeof(time_t)); return 0; } ```

Compile and run that. If you get `8`, you are on a 64-bit `time_t`. If you get `4`, you have a problem that needs addressing before 2038. Most modern glibc-based distros (glibc 2.34+ on x86_64) use 64-bit `time_t` by default, but embedded systems and older ARM builds often do not.

The kernel exposes the current epoch time through the `clock_gettime(CLOCK_REALTIME, ...)` syscall. That is what every userspace tool ultimately calls. CLOCK_REALTIME is adjustable - NTP can step or slew it. CLOCK_MONOTONIC cannot be set backward and is what you should use for measuring elapsed time in applications, not wall clock time.

# Check current epoch time via proc (no external tools)
cat /proc/driver/rtc | grep -i epoch
# Or directly from the kernel's clock
python3 -c "import time; print(int(time.time()))"

Getting Epoch Time on the Command Line

The fastest way to get the current epoch timestamp on any system with GNU coreutils is `date +%s`. That single format specifier returns seconds since the epoch as a plain integer, no newline issues, ready to pipe or assign.

For nanosecond precision, use `date +%s%N`. Be aware that `%N` is a GNU extension and is not available on BSD `date` (macOS included). On macOS, use `gdate` from Homebrew (part of the `coreutils` package) to get GNU behavior.

If you need sub-second precision in a shell script and cannot install extra packages, Python is always available on modern systems:

```bash python3 -c "import time; print(time.time())" ```

That gives you a float like `1753042800.347291`. For integer seconds, wrap it in `int()`. For milliseconds as used in JavaScript timestamps and many APIs:

```bash python3 -c "import time; print(int(time.time() * 1000))" ```

Perl works too, and is often available on systems where Python is not:

```bash perl -e 'print time(), "\n"' ```

In our testing on RHEL 9.3 and Debian 12, `date +%s` executes in roughly 1ms wall time. The Python invocation takes 40-80ms due to interpreter startup. For tight loops in shell scripts, use `date +%s` exclusively.

# Assign current epoch to a variable
NOW=$(date +%s)
echo "Current epoch: $NOW"

# Epoch with milliseconds (GNU date)
date +%s%3N

# On macOS with coreutils installed
gdate +%s

Converting Epoch Timestamps to Human-Readable Dates

The `-d` flag in GNU `date` accepts epoch input with the `@` prefix. This is the cleanest way to convert a raw timestamp back to a readable format without writing any code.

Timezone handling is where engineers get burned. By default, `date` converts to your local timezone as set by `$TZ` or `/etc/localtime`. For consistent output across servers - especially important when correlating logs from machines in different regions - always specify UTC explicitly.

You can combine format specifiers to get exactly the output you need. ISO 8601 is the right choice for log files and API responses because it is unambiguous and sortable:

```bash # ISO 8601 UTC output date -u -d @1753042800 +"%Y-%m-%dT%H:%M:%SZ" # Output: 2025-07-20T21:00:00Z

# Local time with timezone offset date -d @1753042800 +"%Y-%m-%d %H:%M:%S %Z" ```

On BSD/macOS, the syntax differs. BSD `date` uses `-r` for epoch input rather than `-d @`:

```bash # BSD date date -r 1753042800 -u +"%Y-%m-%dT%H:%M:%SZ" ```

For bulk conversion of timestamps in log files, `awk` with `strftime` is the right tool. GNU awk has `strftime` built in:

```bash # Convert all epoch timestamps in column 1 of a log file awk '{print strftime("%Y-%m-%d %H:%M:%S", $1), $0}' access.log ```

If you are processing JSON logs, `jq` handles epoch conversion natively:

```bash jq '.timestamp | todate' events.json # todate converts epoch seconds to ISO 8601 UTC string ```

# Quick reference: convert epoch in both directions

# Now to epoch
date +%s

# Epoch to UTC ISO 8601 (GNU)
date -u -d @EPOCH_HERE +"%Y-%m-%dT%H:%M:%SZ"

# Epoch to local time with timezone
date -d @EPOCH_HERE +"%F %T %Z"

# Human date to epoch
date -d "2026-01-01 00:00:00 UTC" +%s
// advertisement

Converting Human Dates to Epoch

Going the other direction - a human-readable date string into an epoch integer - is equally common. You need this when calculating cutoff timestamps for log rotation, setting token expiry, or constructing API queries that accept epoch parameters.

GNU `date -d` handles this with natural language inputs as well as structured formats:

```bash # Specific UTC datetime to epoch date -d "2026-12-31 23:59:59 UTC" +%s

# Relative time expressions date -d "now + 30 days" +%s date -d "yesterday" +%s date -d "last monday" +%s date -d "next year" +%s ```

Always include the timezone explicitly when converting human dates. `date -d "2026-01-01 00:00:00" +%s` will use the local timezone. If your script runs on a server in UTC and you test it locally in EST, you get different results. Append `UTC` or `+0000` to eliminate ambiguity.

Python's `datetime` module is more explicit and portable for scripted conversions:

```python from datetime import datetime, timezone dt = datetime(2026, 12, 31, 23, 59, 59, tzinfo=timezone.utc) print(int(dt.timestamp())) ```

For shell scripts that need to calculate time deltas - for example, checking whether a file is older than 7 days:

```bash FILE_EPOCH=$(stat -c %Y /path/to/file) NOW=$(date +%s) AGE=$(( NOW - FILE_EPOCH )) SEVEN_DAYS=$(( 7 * 24 * 3600 )) if [ "$AGE" -gt "$SEVEN_DAYS" ]; then echo "File is older than 7 days" fi ```

`stat -c %Y` returns the file's modification time as an epoch integer. This is more reliable than parsing `ls -l` output, which varies by locale and timezone.

# All-in-one: calculate days since a past date
PAST=$(date -d "2026-01-01 UTC" +%s)
NOW=$(date +%s)
DIFF=$(( (NOW - PAST) / 86400 ))
echo "Days elapsed since 2026-01-01: $DIFF"

Epoch Time in Scripts, Cron, and Log Rotation

Epoch timestamps are the right choice for any script that needs to compare times, calculate durations, or create time-based filenames. String-based dates require parsing; epoch integers require subtraction.

For log rotation scripts, epoch-based filenames ensure correct sort order regardless of locale:

```bash #!/bin/bash LOG_DIR=/var/log/myapp ARCHIVE_DIR=/var/log/myapp/archive STAMP=$(date +%s) mv "$LOG_DIR/app.log" "$ARCHIVE_DIR/app-${STAMP}.log" gzip "$ARCHIVE_DIR/app-${STAMP}.log" ```

Those files sort correctly with `ls -1 | sort` because epoch integers are monotonically increasing. YYYY-MM-DD filenames sort correctly too, but epoch stamps avoid any timezone ambiguity in the filename itself.

For cron jobs that need to avoid overlapping runs, an epoch-based lock file with a timeout check is more reliable than a simple PID file:

```bash LOCKFILE=/var/run/myjob.lock LOCK_TIMEOUT=3600

if [ -f "$LOCKFILE" ]; then LOCK_TIME=$(cat "$LOCKFILE") NOW=$(date +%s) AGE=$(( NOW - LOCK_TIME )) if [ "$AGE" -lt "$LOCK_TIMEOUT" ]; then echo "Job already running (lock age: ${AGE}s). Exiting." exit 1 fi echo "Stale lock detected (age: ${AGE}s). Removing." fi

date +%s > "$LOCKFILE" trap "rm -f $LOCKFILE" EXIT

# ... job logic here ```

This pattern handles crashed runs correctly: the lock file persists, but the next invocation detects that it is older than the timeout threshold and proceeds. A pure PID-based lock fails when PIDs are recycled.

In DevOps pipelines, epoch timestamps are useful for deployment tracking. If you use an automation platform like taskbotshub.ai for orchestrating multi-step deployments, embedding the epoch at deploy time gives you a precise, comparable timestamp that survives log aggregation across multiple systems without timezone conflicts.

# Generate a deployment ID combining git short SHA and epoch
DEPLOY_ID="$(git rev-parse --short HEAD)-$(date +%s)"
echo "Deploy ID: $DEPLOY_ID"
# Example output: a3f9b12-1753042800

Epoch Time in Databases and APIs

PostgreSQL stores timestamps internally as microseconds since 2000-01-01, not Unix epoch. But it converts cleanly:

```sql -- Current epoch in PostgreSQL SELECT EXTRACT(EPOCH FROM NOW())::BIGINT;

-- Convert epoch to timestamp SELECT TO_TIMESTAMP(1753042800);

-- Convert timestamp column to epoch for export SELECT EXTRACT(EPOCH FROM created_at)::BIGINT FROM events; ```

MySQL and MariaDB use `UNIX_TIMESTAMP()` and `FROM_UNIXTIME()`:

```sql -- Current epoch SELECT UNIX_TIMESTAMP();

-- Convert stored timestamp to epoch SELECT UNIX_TIMESTAMP(created_at) FROM events;

-- Convert epoch to datetime SELECT FROM_UNIXTIME(1753042800); ```

In Redis, epoch timestamps are the standard for key expiry. The `EXPIREAT` command takes an absolute epoch:

```bash # Set key to expire at a specific epoch timestamp redis-cli EXPIREAT mykey 1753042800

# Check remaining TTL (returns seconds) redis-cli TTL mykey

# EXPIREAT accepts epoch in seconds; PEXPIREAT accepts milliseconds redis-cli PEXPIREAT mykey 1753042800000 ```

JWT tokens use epoch timestamps in their `iat` (issued at) and `exp` (expiry) claims. Both are epoch integers in seconds. A common debugging step when a JWT is rejected:

```bash # Decode JWT payload without verifying signature TOKEN="your.jwt.token" echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Then convert the exp field date -u -d @EXP_VALUE +"%Y-%m-%dT%H:%M:%SZ" ```

HTTP headers use RFC 2822 / RFC 7231 date strings (`Date:`, `Last-Modified:`, `Expires:`), not epoch. Converting between them in curl output:

```bash # Extract Last-Modified from response and convert to epoch LM=$(curl -sI https://example.com | grep -i last-modified | cut -d' ' -f2-) date -d "$LM" +%s ```

-- PostgreSQL: find records created in the last 24 hours using epoch arithmetic
SELECT * FROM events
WHERE EXTRACT(EPOCH FROM created_at) > EXTRACT(EPOCH FROM NOW()) - 86400;
// advertisement

Leap Seconds and Why Epoch Time Ignores Them

Unix epoch time does not count leap seconds. The POSIX specification explicitly defines a day as exactly 86,400 seconds. In reality, since 1972, 27 leap seconds have been inserted by the IERS to keep UTC aligned with astronomical time. Unix time pretends those seconds do not exist.

This means two things. First, converting between Unix time and true UTC is slightly off by 27 seconds if you are doing precise astronomical or physics calculations. For sysadmin work, this is irrelevant. Second, on the rare occasions when a leap second is inserted (the last was in December 2016, and the ITU is working to eliminate them by 2035), NTP may slew your clock or repeat a second. Systems configured with `leapsmear` (Google's approach) spread the correction over a window rather than repeating or skipping a second.

Check your NTP leap second handling:

```bash # Check chrony's leap second status chronyc tracking | grep -i leap

# ntpd ntpq -p | head ntpdc -c kerninfo | grep leap ```

For most production systems, the correct answer is to run chrony with a time server that handles leap smearing, and not worry about the 27-second discrepancy unless you are correlating with GPS or astronomical data.

# chrony.conf snippet for Google's NTP servers which handle leap smearing
server time1.google.com iburst
server time2.google.com iburst
server time3.google.com iburst
server time4.google.com iburst

The 2038 Problem on Production Systems

January 19, 2038 at 03:14:08 UTC is when 32-bit signed `time_t` overflows. Any system or application storing epoch time as a 32-bit integer will either wrap to a large negative number or clamp at zero. This is not hypothetical: embedded systems, older database schemas using INT columns for timestamps, and some file systems are genuinely at risk.

Audit your systems now:

```bash # Find C/C++ binaries compiled with 32-bit time_t on a 64-bit system grep -r 'time_t' /usr/include/bits/types.h

# Check MySQL/MariaDB TIMESTAMP columns (affected - they use 32-bit internally) # DATETIME columns are safe; they store as a string internally mysql -e "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE DATA_TYPE = 'timestamp'"

# PostgreSQL TIMESTAMP is safe - it uses 8-byte representation ```

MySQL's `TIMESTAMP` type is the most common production risk. It stores values as 32-bit UTC epoch seconds and has a maximum value of `2038-01-19 03:14:07 UTC`. Any `TIMESTAMP` column storing dates beyond that will fail. The fix is to migrate to `DATETIME`, which supports up to `9999-12-31`.

```sql -- Migrate a MySQL TIMESTAMP column to DATETIME ALTER TABLE events MODIFY created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6); ```

Filesystem timestamps are another vector. ext3 on 32-bit systems has the 2038 problem. ext4 uses 34-bit timestamps (with 2 extra bits from a reserved field) extending coverage to 2446. XFS uses 32-bit timestamps but has been updated in Linux kernel 5.10+ to handle dates beyond 2038 by using the signed range differently - check your kernel version and XFS version.

For new projects using integer columns to store timestamps, use `BIGINT` in your schema definitions. This is a twelve-second conversation to have now versus a potential production incident twelve years from now.

# Simulate what happens at 2038 overflow (do NOT run on production)
# Just see the value
date -d @2147483648
# GNU date will handle this correctly on 64-bit systems
# A 32-bit system or database would show 1901-12-13 or similar

Practical Debugging with Epoch Timestamps

When correlating logs across multiple servers, epoch timestamps eliminate timezone confusion entirely. Configure your application logs to emit epoch floats rather than human-readable strings. For nginx, add `$msec` to your log format:

```nginx log_format epoch '$msec $remote_addr $request $status $body_bytes_sent'; access_log /var/log/nginx/access.log epoch; ```

`$msec` is seconds.milliseconds since epoch. That value sorts correctly, compares correctly, and needs no parsing to calculate durations between log lines.

For systemd journal, `journalctl` can output epoch timestamps with `--output=short-unix`:

```bash journalctl --output=short-unix -u sshd --since "1 hour ago" # Output includes epoch float as first column ```

When debugging a sequence of events across a distributed system, pull epoch timestamps from each source and sort them together:

```bash # Merge and sort log lines from two servers by epoch timestamp cat server1.log server2.log | sort -k1,1n | head -50 ```

This works only if both logs emit epoch as the first whitespace-delimited field, which is another argument for standardizing on that format.

For ad-hoc timestamp queries in shell scripts, create a small function in your `.bashrc` or team's shared shell library:

```bash # Convert any epoch or ISO date to the other format epoch() { if [[ "$1" =~ ^[0-9]{10,13}$ ]]; then local ts=$1 # Normalize milliseconds to seconds [[ ${#ts} -eq 13 ]] && ts=$(( ts / 1000 )) date -u -d "@${ts}" +"%Y-%m-%dT%H:%M:%SZ" else date -d "$*" +%s fi } ```

That function handles both directions: feed it an epoch integer and get ISO 8601 back; feed it a date string and get an epoch integer back. In our experience, this single function eliminates 80% of the manual timestamp conversion work in debugging sessions.

# Find all log entries in a 5-minute window around an incident
# Incident at epoch 1753042800
START=$(( 1753042800 - 150 ))
END=$(( 1753042800 + 150 ))
awk -F'[ .]' '$1 >= '"$START"' && $1 <= '"$END"'' /var/log/nginx/access.log
// advertisement