Timezones in Linux

Timezones in Linux
Chapter 10 Series — Part 6: TZ Variable, tzset(), and /usr/share/zoneinfo
TZ
Environment Variable
tzset()
Initialiser Function
zoneinfo
Timezone Database

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

TZ environment variable tzset() /usr/share/zoneinfo /etc/localtime DST (Daylight Saving Time) UTC offset tzname[]

📁 The Timezone Database: /usr/share/zoneinfo

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

🏭 Setting the TZ Environment Variable

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.

🔧 tzset() — How Functions Read the TZ Variable

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 */

📈 How Timezone Resolution Works — Step by Step
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().

Next: Linux Locales & setlocale() → All Linux Posts

Leave a Reply

Your email address will not be published. Required fields are marked *