A reliable MariaDB time zone configuration starts by identifying what a value means. An order creation time is an instant on a global timeline. A shop that opens at 09:00 uses local wall-clock time. A recurring meeting at 09:00 Europe/Berlin needs a zone identifier because its UTC offset changes with daylight saving rules. Storing all three as an unlabeled DATETIME creates ambiguity that configuration alone cannot repair.
MariaDB adds another layer: the global time_zone supplies a default to new sessions, each connection can override its session zone, TIMESTAMP converts between session time and UTC storage, DATETIME preserves written calendar fields without automatic zone conversion, and named-zone functions require loaded time-zone tables. Operating-system clocks, container images, drivers, schedulers, replicas, dumps, and analytics systems must agree with the chosen model.
This guide defines a UTC-first production policy, compares temporal types, loads and maintains named zones, tests daylight-saving gaps and overlaps, migrates legacy values, validates application pools, monitors clock and configuration drift, and provides a rollback that does not reinterpret data silently.
Model instants and wall-clock values separately
Use precise concepts:
- Instant: one point on the UTC timeline, such as a payment authorization.
- Offset: a numeric displacement from UTC, such as
+02:00; it has no future DST rules. - Named time zone: an IANA identifier such as
Europe/Berlin, with historical and future transition rules. - Local date/time: calendar fields without a zone, such as
2026-10-25 02:30:00. - Duration: elapsed time, not a clock display.
- Recurrence: a rule such as every Monday at 09:00 in a named zone.
For audit events, row creation, job execution, token expiry, and payment processing, store an unambiguous UTC instant. Convert to the viewer's zone at the presentation boundary.
For appointments and recurring schedules, store the local fields plus the IANA zone and the business rule used to handle nonexistent or repeated local times. A numeric offset alone is insufficient for future schedules because governments can change rules.
For a birthday or due date without a time, use DATE, not midnight UTC. Converting midnight between zones can move the displayed calendar date.
Compare TIMESTAMP and DATETIME
MariaDB's two common date-time types behave differently:
| Property | TIMESTAMP |
DATETIME |
|---|---|---|
| Storage meaning | UTC-based seconds internally | Calendar fields |
| Session-zone conversion | On insert and retrieval | No automatic zone conversion |
| Range | Release dependent, historically limited | Year 1000 through 9999 |
| Typical use | Instants in supported range | Wall-clock values or explicit UTC convention |
| Microseconds | Precision 0–6 | Precision 0–6 |
| Automatic default/update | Supported with explicit clauses | Supported on modern MariaDB with explicit clauses |
TIMESTAMP is useful for instants because MariaDB converts input from the session zone to UTC and converts stored UTC to the current session zone when selecting it. The same row can therefore display differently in two sessions.
DATETIME stores the fields supplied. Writing 2026-08-25 12:00:00 and reading it under another session zone still returns those fields. The column does not record whether they meant UTC, Berlin, or Ho Chi Minh City. Your schema and application contract must supply that meaning.
Do not call DATETIME “timezone unaware” and assume it is wrong. It is appropriate for wall-clock schedules and for UTC instants when an application explicitly normalizes and documents them. The risk is unlabeled semantics.
Version-gate TIMESTAMP range
On MariaDB before 11.5, the documented TIMESTAMP range ends in January 2038. MariaDB 11.5 and newer extend the upper range into February 2106. Query the actual server:
SELECT VERSION();
Do not use TIMESTAMP for birthdays, century-scale retention, certificate dates, leases extending beyond the supported range, or future schedules without checking every primary, replica, restore target, and downgrade path.
MariaDB's historical first-TIMESTAMP implicit default behavior also depends on explicit_defaults_for_timestamp and release. Define defaults explicitly:
CREATE TABLE orders.order_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL
DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
) ENGINE=InnoDB;
Never depend on “the first timestamp updates automatically” behavior. Explicit DDL survives upgrades and is easier to audit.
Audit operating-system time first
MariaDB cannot compensate for a host clock that is wrong. On Linux:
timedatectl status
date --iso-8601=seconds
date --utc --iso-8601=seconds
Inspect time synchronization:
timedatectl timesync-status 2>/dev/null || true
systemctl status systemd-timesyncd --no-pager 2>/dev/null || true
chronyc tracking 2>/dev/null || true
Use the organization's NTP or time-sync service and monitor offset. Do not run multiple competing synchronization daemons without a deliberate design.
The OS display zone does not need to be local business time. UTC on database hosts and containers reduces operator confusion. Named business zones belong in data and presentation logic.
Audit MariaDB global and session zones
Inspect both server-derived and active settings:
SELECT @@system_time_zone,
@@GLOBAL.time_zone,
@@SESSION.time_zone,
NOW(6) AS session_now,
UTC_TIMESTAMP(6) AS utc_now;
system_time_zone is determined when MariaDB starts from the operating-system environment or TZ. It is read-only during runtime. The special time_zone='SYSTEM' tells a session to use that system zone.
The global time_zone supplies the default for new connections. Existing sessions keep their session setting when the global value changes.
Inspect origin metadata where supported:
SELECT VARIABLE_NAME, GLOBAL_VALUE, SESSION_VALUE,
GLOBAL_VALUE_ORIGIN, DEFAULT_VALUE
FROM information_schema.SYSTEM_VARIABLES
WHERE VARIABLE_NAME IN ('TIME_ZONE','SYSTEM_TIME_ZONE');
Column availability can vary. Use DESCRIBE information_schema.SYSTEM_VARIABLES before automating.
Query through the exact application connection as well. A CLI session does not reveal what a driver, pool initialization hook, proxy, or ORM sets.
Standardize new service sessions on UTC
For most application services, use UTC explicitly:
SET SESSION time_zone = '+00:00';
Numeric +00:00 works without populated named-zone tables. Many connectors can run an initialization statement when creating each physical pool connection. Use the driver-supported mechanism and verify it after checkout:
SELECT @@SESSION.time_zone, NOW(6), UTC_TIMESTAMP(6);
Setting a server-wide UTC default reduces surprises:
[mariadb]
default_time_zone = +00:00
On Ubuntu, place it in an owned late-loading file. Inspect parsed defaults, restart in a planned window, then verify a new session.
Changing only the global default does not rewrite stored data and does not change existing sessions. Pool recycling is part of rollout.
Choose named zones rather than abbreviations
Use IANA identifiers such as:
Europe/Berlin
Asia/Ho_Chi_Minh
America/New_York
Avoid abbreviations such as CST, which can mean different regions. Avoid storing only +01:00 for a future Berlin event; the summer offset may be +02:00, and transition rules can change.
Store the zone name in a validated column:
CREATE TABLE scheduling.appointments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
local_start DATETIME(6) NOT NULL,
time_zone_name VARCHAR(64) NOT NULL,
resolved_start_utc DATETIME(6) NOT NULL,
PRIMARY KEY (id),
KEY ix_appointments_start_utc (resolved_start_utc)
) CHARACTER SET utf8mb4;
Validate time_zone_name against a supported allow-list in the application. Keep the original local fields and zone for future re-evaluation when rules change. The resolved UTC instant supports efficient execution and ordering, but define whether existing appointments are recomputed after tzdata updates.
Inspect named-zone table availability
MariaDB's mysql.time_zone* tables are often empty by default. Check them:
SELECT COUNT(*) AS named_zones
FROM mysql.time_zone_name;
SELECT Name
FROM mysql.time_zone_name
WHERE Name IN ('UTC','Europe/Berlin','Asia/Ho_Chi_Minh')
ORDER BY Name;
Test conversion:
SELECT CONVERT_TZ(
'2026-01-15 12:00:00',
'UTC',
'Europe/Berlin'
) AS berlin_time;
CONVERT_TZ() returns NULL for invalid arguments or unavailable named zones. Do not treat NULL silently as the original time.
Load named time zones safely
On Linux, mariadb-tzinfo-to-sql reads the operating-system zoneinfo database and generates SQL for MariaDB's time-zone tables. Capture versions first:
mariadb-tzinfo-to-sql --version 2>/dev/null || true
dpkg-query -W -f='${Package} ${Version}n' tzdata mariadb-client
Back up the mysql system database through a supported, version-matched procedure. Test loading on staging:
mariadb-tzinfo-to-sql /usr/share/zoneinfo |
sudo mariadb mysql
This modifies MariaDB system tables. Do not run it casually in production. Ensure the input directory is the real zoneinfo tree and not a recursive structure containing unsuitable files.
MariaDB documentation recommends restarting after population so new data is correctly loaded:
sudo systemctl restart mariadb
sudo systemctl --no-pager --full status mariadb
sudo journalctl -u mariadb -n 100 --no-pager
Then test known winter and summer conversions. Do not truncate time-zone tables as a rollback unless a reviewed recovery specifically requires it; truncation is destructive and old in-memory values can persist until restart.
Maintain tzdata as production data
Governments change offsets and daylight-saving rules. Updating the OS tzdata package does not automatically refresh MariaDB's copied time-zone tables. Establish a lifecycle:
- Monitor tzdata releases relevant to supported regions.
- Update OS packages in staging.
- Reload MariaDB time-zone tables with matching tooling.
- Restart according to the tested procedure.
- Run transition and business-schedule tests.
- Roll through replicas or cluster nodes safely.
- Decide whether future scheduled instants need recalculation.
- Record tzdata version and deployment time.
If application runtimes also ship their own zone database, update and test them in the same release program. MariaDB and a Java or Python service can disagree when their tzdata versions differ.
Test daylight-saving gaps and overlaps
When clocks move forward, some local times do not exist. When they move backward, some local times occur twice. The exact dates differ by zone and year.
For a gap, an application must choose a policy:
- Reject the nonexistent local time and ask the user.
- Move to the next valid time.
- Apply a documented business-specific rule.
For an overlap, choose:
- Earlier occurrence.
- Later occurrence.
- Require an explicit offset.
- Store an additional disambiguation flag.
Do not let a library's undocumented default choose silently. Build tests from authoritative transitions in the deployed tzdata. Compare application conversion with MariaDB:
SELECT CONVERT_TZ(
'2026-07-15 09:00:00',
'Europe/Berlin',
'UTC'
) AS summer_utc,
CONVERT_TZ(
'2026-01-15 09:00:00',
'Europe/Berlin',
'UTC'
) AS winter_utc;
The different results demonstrate why a fixed offset cannot represent a named zone across seasons.
Use UTC ranges for indexed queries
Applying a function to every indexed column value can prevent efficient range access:
# Avoid converting the indexed column row by row for a broad range.
SELECT *
FROM order_events
WHERE CONVERT_TZ(created_at, 'UTC', 'Europe/Berlin')
>= '2026-08-25 00:00:00';
Instead, convert the requested local boundary to UTC once, then compare the raw indexed instant:
SET @start_utc = CONVERT_TZ(
'2026-08-25 00:00:00',
'Europe/Berlin',
'UTC'
);
SET @end_utc = CONVERT_TZ(
'2026-08-26 00:00:00',
'Europe/Berlin',
'UTC'
);
SELECT *
FROM order_events
WHERE created_at >= @start_utc
AND created_at < @end_utc;
Use half-open ranges to avoid double-counting boundaries and microseconds. Validate that @start_utc and @end_utc are non-NULL before querying.
Understand CURRENT_TIMESTAMP and related functions
In a session:
SELECT NOW(6), CURRENT_TIMESTAMP(6),
UTC_TIMESTAMP(6), SYSDATE(6);
NOW() and CURRENT_TIMESTAMP represent the statement or transaction-related current time according to MariaDB semantics and session zone. UTC_TIMESTAMP() returns UTC calendar fields. SYSDATE() has different evaluation semantics and can be affected by the sysdate_is_now server option.
Do not mix these functions casually in auditing, replication-sensitive logic, generated identifiers, or duration measurement. Database wall clocks can adjust through NTP. Use monotonic clocks in application code for elapsed-duration measurement.
For row creation, an explicit TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) under a UTC session policy is straightforward. For immutable business audit records, also preserve actor, source, transaction context, and an application correlation identifier; a timestamp alone is not an audit trail.
Audit temporal schema semantics
Inventory temporal columns:
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,
COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
FROM information_schema.COLUMNS
WHERE DATA_TYPE IN
('date','time','datetime','timestamp','year')
AND TABLE_SCHEMA NOT IN
('mysql','information_schema','performance_schema','sys')
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION;
For every column, classify:
- Instant stored as
TIMESTAMP. - UTC instant stored by convention in
DATETIME. - Local wall-clock date/time.
- Date-only business value.
- Duration or time-of-day.
- Legacy value with unknown semantics.
Column names such as created_at suggest meaning but are not proof. Inspect write code, old documentation, source-system exports, and values around known timezone transitions.
Check explicit defaults and automatic update clauses with SHOW CREATE TABLE:
SHOW CREATE TABLE orders.order_events;
Do not bulk change every DATETIME to TIMESTAMP. Range, automatic conversion, wall-clock semantics, and 2038 compatibility differ.
Migrate legacy local DATETIME values
Suppose a legacy column contains Berlin local fields but no zone metadata. A migration to UTC requires these prerequisites:
- Prove all rows use the same source zone or classify them.
- Load and version the correct historical named-zone rules.
- Detect gap and overlap values requiring a policy.
- Back up and restore-test the table.
- Stop or dual-write new legacy values during conversion.
- Validate counts and sampled byte-for-byte source fields.
Prefer a new column rather than in-place reinterpretation:
ALTER TABLE legacy.appointments
ADD COLUMN starts_at_utc DATETIME(6) NULL,
ADD COLUMN source_time_zone VARCHAR(64) NULL;
Populate a bounded batch in staging:
UPDATE legacy.appointments
SET starts_at_utc = CONVERT_TZ(
local_start,
'Europe/Berlin',
'UTC'
),
source_time_zone = 'Europe/Berlin'
WHERE id BETWEEN 1 AND 10000
AND starts_at_utc IS NULL;
Before committing production batches, find conversions returning NULL, compare seasonal offsets, inspect DST transition rows, and reconcile every count. The update is not safe for ambiguous overlap times unless the business rule selects which occurrence was intended.
After dual-read validation, switch the application to the UTC column. Keep the source local field and zone when future reinterpretation or user display requires them. Remove legacy columns only after retention and rollback windows.
Migrate TIMESTAMP or session behavior carefully
Changing the global session default can alter how the same TIMESTAMP row displays and how new literal values are interpreted. Rollout order matters:
- Make application writes explicitly UTC or zone-aware.
- Add round-trip tests through each connector.
- Canary
SET SESSION time_zone='+00:00'on a small cohort. - Compare raw instants and user-visible output.
- Update readers to format in the user zone.
- Change global default for new sessions.
- Recycle pools.
- Persist
default_time_zoneand test restart.
Do not run UPDATE timestamp_column = timestamp_column to “convert” stored TIMESTAMP values. They are already stored as UTC-based instants; session conversion affects interpretation and display.
For DATETIME values stored by UTC convention, changing session zone does not transform them. Application code must continue treating them as UTC explicitly.
Handle replication, Galera, and failover
Keep OS clocks synchronized across every node. Compare:
SELECT @@hostname, @@server_id,
@@system_time_zone,
@@GLOBAL.time_zone,
NOW(6), UTC_TIMESTAMP(6);
Named-zone tables belong to the mysql system database and require a deliberate distribution/refresh plan. Do not assume ordinary application-schema replication or a managed service copies them identically.
Test events, scheduled jobs, statement-based binary logging where used, generated values, failover, and restores. Session state may be recorded or applied differently depending on statement and replication format, but identical tzdata and explicit UTC semantics reduce risk.
In Galera, update tzdata and time-zone tables through a procedure compatible with cluster system-table behavior. Roll nodes safely, verify membership, and test conversions on each node before returning traffic.
Handle containers and Kubernetes
Minimal images may omit /usr/share/zoneinfo or carry stale tzdata. The MariaDB server can still run in UTC, but named-zone loading from that container will fail or use old rules.
Inspect inside the image:
kubectl -n data exec mariadb-0 -- date --iso-8601=seconds
kubectl -n data exec mariadb-0 -- date --utc --iso-8601=seconds
kubectl -n data exec mariadb-0 --
sh -c 'test -d /usr/share/zoneinfo && echo zoneinfo-present'
Avoid mounting the host's /etc/localtime just to make logs appear local. UTC logs with explicit offsets are easier to correlate. Pin and update image tzdata through the build pipeline.
ConfigMaps can set TZ, but MariaDB reads system_time_zone at process startup. A changed ConfigMap does not guarantee the pod restarted or the database default changed. Verify actual session values after every rollout.
Test backups, dumps, and restores
Logical dumps may contain session SET time_zone statements to preserve temporal interpretation. Use version-matched dump tools and do not remove those lines without understanding them.
Restore into an isolated server whose OS clock, tzdata, MariaDB time-zone tables, sql_mode, and session defaults are known. Validate:
- Representative
TIMESTAMPvalues before and after restore. DATETIMEfields by intended convention.- Stored events and routines.
- Automatic defaults and update clauses.
- Named-zone conversions.
- Values near 2038 for pre-11.5 targets.
- DST transition cases.
A successful restore command does not prove temporal equivalence.
Monitor time and timezone drift
Collect:
- NTP offset and synchronization state.
- MariaDB
system_time_zone, globaltime_zone, and version. - Application session zone through a canary connection.
- OS and runtime tzdata versions.
- Named-zone table row count and a controlled conversion result.
- Differences between
NOW()andUTC_TIMESTAMP()expected for the session. - Scheduler execution times versus expected UTC instants.
- Error counts from invalid or out-of-range temporal values.
Use a canary test that creates no business data:
SELECT @@SESSION.time_zone,
UTC_TIMESTAMP(6) AS utc_now,
CONVERT_TZ(
'2026-07-15 09:00:00',
'Europe/Berlin',
'UTC'
) AS known_conversion;
Alert when known_conversion becomes NULL or differs from the approved tzdata expectation. Update the expected value deliberately when rules change.
Troubleshoot common timezone failures
CONVERT_TZ returns NULL
The named zone may be absent, an argument invalid, or the conversion outside the supported range. Check mysql.time_zone_name, use a known fixed-offset control, inspect version, and reload tables through the supported process.
Application and CLI show different TIMESTAMP values
Compare @@SESSION.time_zone in both connections. The pool or driver may set its own zone. The row can be identical while display differs.
Time is off by exactly one or two hours
This often indicates a local/UTC mismatch or DST assumption. Inspect source meaning, session zone, named-zone rules, application formatting, and current date. Do not subtract a hard-coded hour.
Time is off by several hours only in one environment
Compare OS TZ, system_time_zone, global/session settings, container image, driver initialization, and proxy behavior. Environment-specific defaults are the likely cause.
Duplicate or missing scheduled execution near DST
The scheduler interpreted local recurrence through a gap or overlap. Store zone and disambiguation policy, calculate next UTC occurrence explicitly, and make jobs idempotent.
TIMESTAMP rejects a future date
Check MariaDB version. Pre-11.5 releases have the 2038 upper limit. Use DATETIME with explicit instant semantics or upgrade after full compatibility testing.
Historical times change after tzdata update
Named-zone rules may have been corrected. Decide whether stored UTC instants remain authoritative or future schedules should be re-resolved. Do not bulk rewrite history without a policy.
Logs cannot be correlated
Ensure every log includes an ISO 8601 timestamp with offset or UTC Z, host, service, and request ID. Align clocks and parsing. A local timestamp without an offset is ambiguous.
Roll back safely
A configuration rollback can restore the previous global default:
SET GLOBAL time_zone = 'SYSTEM';
Or restore a previous explicit value. This affects new sessions only. Revert the option file, restart if required, and recycle pools.
Data migration rollback is harder. Reversing UTC conversion requires the original zone and overlap choice. Preserve source columns, zone names, migration version, and pre-migration backup until validation completes.
Do not truncate named-zone tables as an emergency first step. That can break every named conversion and requires restart to clear cached behavior. Restore the tested system-table backup or previous image/tzdata procedure.
Production best practices
- Store instants in UTC and convert at boundaries.
- Store local fields plus an IANA zone for future wall-clock schedules.
- Use
DATEfor date-only business values. - Choose
TIMESTAMPversusDATETIMEfrom semantics and range. - Set every application session to UTC explicitly.
- Keep OS, database, and runtime clocks synchronized.
- Load and maintain named-zone tables through a tested lifecycle.
- Version-gate the 2038/2106
TIMESTAMPrange. - Test DST gaps and overlaps for every supported business zone.
- Query indexed UTC ranges rather than converting every row.
- Preserve source zone and ambiguity decisions in migrations.
- Test dumps, restores, replicas, Galera, and failover.
- Monitor tzdata versions, named conversions, and session drift.
- Keep logs in UTC or include an explicit numeric offset.
FAQ
Should MariaDB always run in UTC?
UTC is the safest default for server and application sessions. Local wall-clock schedules still need their IANA zone and recurrence rules stored explicitly.
Does DATETIME convert between time zones?
No. It preserves calendar fields. Your application or SQL must define and convert its meaning.
Does TIMESTAMP store a timezone name?
No. It stores a UTC-based instant and converts using the session zone. It does not preserve the original zone identifier.
Why does CONVERT_TZ return NULL for named zones?
The MariaDB time-zone tables may be empty or stale, an argument invalid, or the value outside the supported range.
Is a numeric UTC offset enough for future appointments?
No. An offset has no daylight-saving or political transition rules. Store an IANA zone such as Europe/Berlin.
Does changing global time_zone affect current connections?
No. New sessions inherit it. Existing pooled sessions keep their current session value until changed or reconnected.
Does MariaDB have the year 2038 problem?
For TIMESTAMP, releases before MariaDB 11.5 have the historical 2038 limit. MariaDB 11.5 extends the upper range to 2106. DATETIME has a much wider range.
Should recurring jobs use local time or UTC?
Store the local recurrence and named zone, resolve each next occurrence to UTC with an explicit DST policy, and make execution idempotent.
Conclusion
A robust MariaDB time zone configuration begins with semantic data modeling. Store global instants as UTC, keep local calendar schedules with their named zone, and use DATE, DATETIME, or TIMESTAMP according to meaning and supported range rather than convenience.
Standardize application sessions, synchronize clocks, maintain named-zone tables and runtime tzdata, and test daylight-saving boundaries as real business cases. With source-zone metadata and staged migration, timezone changes stop being mysterious hour offsets and become a controlled, verifiable part of the platform.
Suggested Internal Links
- Understand MariaDB Configuration Files and Precedence
- Configure MariaDB Character Sets and utf8mb4 Correctly
- Set MariaDB SQL Modes Without Breaking Applications
- Tune MariaDB Connections, Threads, and Timeouts
- Upgrade MariaDB Safely Between Major Versions
- Back Up and Restore MariaDB with mariadb-dump