Why Timezones Are Harder Than They Look
Imagine you are building a meeting scheduler for users in India (IST, UTC+5:30) and Germany (CET, UTC+1 or UTC+2 in summer). Converting between them is not just arithmetic — countries change their Daylight Saving Time rules, governments create new timezones, and some places (like India) have non-integer UTC offsets. Linux handles all this complexity through a timezone database and environment variable system.
Keywords in This Post
Rather than hardcoding timezone rules into every program, Linux stores all timezone data in binary files under /usr/share/zoneinfo/. Each file represents one timezone and encodes its complete history of UTC offsets and DST transitions.
| Path | Represents |
|---|---|
| /usr/share/zoneinfo/UTC | Universal Coordinated Time |
| /usr/share/zoneinfo/Asia/Kolkata | Indian Standard Time (IST, UTC+5:30) |
| /usr/share/zoneinfo/Europe/Berlin | Germany (CET/CEST) |
| /usr/share/zoneinfo/America/New_York | US Eastern Time (EST/EDT) |
| /usr/share/zoneinfo/Pacific/Auckland | New Zealand (NZST/NZDT) |
The system’s default timezone is set by symlinking /etc/localtime to one of these files:
# Check the current system timezone
ls -la /etc/localtime
# Output: /etc/localtime -> /usr/share/zoneinfo/Asia/Kolkata
# List available timezones for a region
ls /usr/share/zoneinfo/Asia/
# View all timezone names
timedatectl list-timezones
The TZ environment variable lets you override the system timezone for a single program run, without root access and without changing the system configuration. This is extremely useful for testing or running services for different regional users.
Method 1: Colon + timezone name (recommended)
# Run a program in New Zealand timezone
TZ=":Pacific/Auckland" ./my_program
# Run the same program in UTC
TZ=":UTC" ./my_program
# Run in Indian time
TZ=":Asia/Kolkata" ./my_program
Setting TZ affects all the time functions that depend on local time: localtime(), mktime(), ctime(), and strftime(). It does not affect gmtime() (which always returns UTC).
Embedded systems tip: On a headless embedded device running in a factory in Germany, you would set TZ=":Europe/Berlin" in the startup script (/etc/profile or a systemd unit file). All log timestamps will then automatically be in local German time.
Method 2: Manual rule string (POSIX format)
# CET: 1 hour ahead of UTC standard, 2 hours ahead during DST
# DST runs last Sunday in March to last Sunday in October
TZ="CET-1:00:00CEST-2:00:00,M3.5.0,M10.5.0" ./my_program
Method 2 is verbose, error-prone, and doesn’t handle historical rule changes. Always prefer Method 1 (colon + zoneinfo name) unless you are writing for a highly embedded system without a zoneinfo database.
When you set TZ, time functions do not automatically pick it up mid-run. They use a function called tzset(), which reads TZ and initialises three global C variables:
#include <time.h>
void tzset(void);
/* After tzset(), these globals are set: */
extern char *tzname[2]; /* [0] = standard TZ name (e.g. "CET"), [1] = DST name (e.g. "CEST") */
extern int daylight; /* Non-zero if timezone has a DST variant */
extern long timezone; /* Seconds west of UTC (positive = west, negative = east) */
| Variable | Example (IST, India) | Example (CET, Germany) |
|---|---|---|
| tzname[0] | “IST” | “CET” |
| tzname[1] | “IST” (India has no DST) | “CEST” |
| daylight | 0 (no DST) | 1 (has DST) |
| timezone | -19800 (= -5.5h × 3600) | -3600 (= -1h × 3600) |
Note on the timezone sign convention: The timezone global stores seconds west of UTC. India (UTC+5:30) is east of UTC, so its value is negative (-19800). This counterintuitive sign is a POSIX historical convention. When displaying offsets to users, negate the value: printf("UTC%+ld\n", -timezone / 3600);
#include <time.h>
#include <stdio.h>
int main(void)
{
tzset(); /* Read TZ and initialise the globals */
printf("Standard timezone : %s\n", tzname[0]);
printf("DST timezone : %s\n", tzname[1]);
printf("Has DST? : %s\n", daylight ? "yes" : "no");
printf("UTC offset (hours): %+.1f\n", (double)(-timezone) / 3600.0);
return 0;
}
/* Run: TZ=":Asia/Kolkata" ./a.out
Output:
Standard timezone : IST
DST timezone : IST
Has DST? : no
UTC offset (hours): +5.5 */
| Step | What happens |
|---|---|
| 1 | Your program calls localtime() or mktime() |
| 2 | The C library calls tzset() internally |
| 3 | tzset() checks if TZ environment variable is set |
| 4a | If TZ is set: load the named timezone file from /usr/share/zoneinfo/ |
| 4b | If TZ is not set: read /etc/localtime (the system default) |
| 4c | If TZ is set to empty string "": use UTC |
| 5 | Apply offset and DST rules from the loaded timezone data to your time_t |
Up Next in This Series
Part 7 covers Locales — how Linux programs adapt their output to different languages and cultural conventions using setlocale().
