You want to render dates and times in the user’s local timezone. The user hasn’t logged in yet (no profile timezone). What’s your best signal? IP-derived timezone — fast, server-side, no JavaScript dependency. The accuracy varies by region but is usually good enough for the common use cases.
This post walks through detecting timezone from IP, when to trust it, when to combine with client-side detection, and the practical patterns for rendering dates correctly.
What “Timezone from IP” Means
IP geolocation maps an IP to a city or region. From the city, you can determine the IANA timezone (e.g., Europe/Berlin, America/Los_Angeles).
A timezone is more than a UTC offset:
- UTC offset changes (DST observed or not).
- IANA timezone includes DST rules and historical changes.
For correctness, use IANA names, not raw offsets.
The Ip2Geo API returns the IANA timezone directly:
const result = await convertIP(ip)
console.log(result.data.timezone) // e.g., "Europe/Berlin"
One API call gives you the timezone string suitable for date libraries.
Accuracy
Country-level geolocation is highly accurate (~99%). City-level varies (~50-80% in well-mapped markets). Timezone is somewhere between, depending on how many timezones a country has:
- US — 6+ timezones; city-level inaccuracy can put you in the wrong timezone, but usually within an hour.
- Russia, China — Many timezones; similar concerns.
- UK, Germany, France, Japan — One timezone for the whole country; very accurate.
For most users, IP-derived timezone is correct. For users near timezone boundaries or in geographically large countries, it might be off by an hour.
For accuracy details in general.
When IP-Timezone Is Wrong
A few cases where you’ll get the wrong answer:
VPN users
Their IP points to the VPN exit’s location. The user’s actual timezone might be 8 hours different.
Mobile users with PGW anchoring
A mobile user in Calgary might exit through a Toronto PGW. Timezone says Toronto; user is on Calgary time.
See mobile network architecture.
Travelers
A user from Berlin on a business trip in New York hits your site. IP says New York; their watch says Berlin. Which is “their” timezone is fuzzy.
Anycast IPs
The CDN POP’s timezone might not match the user.
For these cases, IP-timezone is a starting point, not a final answer.
Combining with Client-Side Detection
Browsers expose timezone via JavaScript:
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
// e.g., "Europe/Berlin"
The browser’s timezone is based on the OS clock — much more reliable than IP for the user’s actual location.
Best practice
- Server-render with IP-derived timezone for the initial response.
- Once JS runs, detect the browser’s timezone and use that going forward.
- For authenticated users, ask once and store in the profile.
This way:
- First-page TTFB is fast (no JS dependency).
- Server-rendered content is approximately correct.
- After JS hydration, things settle to the precise timezone.
Server-Side Rendering Pattern
In your backend:
import { convertIP } from '@ip2geo/sdk'
app.get('/', async (req, res) => {
const result = await convertIP(req.ip)
const timezone = result.success ? result.data.timezone : 'UTC'
res.render('index', {
timezone,
formattedDate: formatDate(new Date(), timezone)
})
})
function formatDate(date: Date, tz: string): string {
return new Intl.DateTimeFormat('en-US', {
timeZone: tz,
dateStyle: 'full',
timeStyle: 'short'
}).format(date)
}
The user sees their (approximate) local date/time without any JS.
Client-Side Update
After hydration, if the IP-derived timezone doesn’t match the browser:
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone
if (browserTz !== window.__SERVER_TZ__) {
// Re-render dates with the correct timezone
document.querySelectorAll('[data-date]').forEach(el => {
const utcDate = new Date(el.dataset.date)
el.textContent = new Intl.DateTimeFormat('en-US', {
timeZone: browserTz,
dateStyle: 'full',
timeStyle: 'short'
}).format(utcDate)
})
}
Updates dates in place once the browser’s actual timezone is known.
Storing Times in UTC
Universal rule: store times in UTC. Convert to local timezone only for display.
// Save
const event = {
createdAt: new Date().toISOString() // ISO 8601 UTC
}
// Display
const display = new Intl.DateTimeFormat(locale, {
timeZone: userTimezone,
dateStyle: 'short',
timeStyle: 'medium'
}).format(new Date(event.createdAt))
This separates storage concerns (always UTC, unambiguous) from presentation concerns (local timezone, per-user).
Special Cases
Booking and scheduling
“Book this meeting at 3pm Pacific” — explicitly include the timezone the user picked. Don’t auto-convert; let the user see and confirm.
Cron jobs
Server-side recurring tasks should run in a fixed timezone (usually UTC) regardless of user timezones. Don’t make scheduling depend on whoever triggered it.
Time-sensitive comparisons
“Did this happen before midnight?” — depends on whose midnight. Be explicit in the data model.
Library Recommendations
Modern JavaScript/TypeScript:
Intl.DateTimeFormat(built-in) — Best for formatting.TemporalAPI (built-in, ES2024+) — Modern date/time replacement forDate.date-fns-tz— Library for IANA timezone handling.luxon— Modern date/time library; native IANA support.dayjswithutcandtimezoneplugins — Lightweight option.
Avoid:
- Raw
Datearithmetic — Doesn’t handle DST or non-UTC timezones correctly. moment.js— Deprecated; use luxon or date-fns instead.
For server-side Node, the same libraries work. Python: pytz or the stdlib zoneinfo (Python 3.9+). PHP: DateTimeZone.
Personalization Examples
A few practical “I have the timezone, now what?” patterns:
Format dates in user’s timezone
“Event at 3:00 PM PDT” (instead of “10:00 PM UTC”)
Schedule notifications respecting quiet hours
Don’t send a push notification at 3am local time.
”Time ago” displays
“2 hours ago” — depends on local time perception. Calculate in the user’s timezone.
Date pickers
Default the date picker to “today” in the user’s timezone, not UTC.
”Open” / “Closed” indicators
For local businesses showing on a map — calculate open status using the business’s timezone, not the user’s.
SEO and Schema.org
For events / hours displayed on a page, use Schema.org Event or LocalBusiness with proper ISO 8601 timestamps including offset:
{
"@type": "Event",
"startDate": "2026-06-01T19:00:00-07:00"
}
Search engines render these in the searcher’s local timezone. Don’t depend on IP-timezone detection for SEO-relevant times.
Privacy Considerations
Timezone is one of many fingerprinting signals. Tor Browser deliberately sets timezone to UTC to avoid leaking the user’s location.
For typical applications, this isn’t a privacy issue — you’re using the timezone the user’s browser reports. But if you log timezone alongside other identifiers, you contribute to potential tracking.
TL;DR
- IP-derived timezone is a fast, server-side signal.
- Use IANA timezone names (
Europe/Berlin), not raw offsets. - Accuracy is good for countries with one timezone; less so for geographically large countries with multiple.
- VPN, mobile, travelers make IP-timezone unreliable.
- Combine with client-side detection for accuracy after page load.
- Store times in UTC; convert to local for display.
- Use modern date libraries — luxon, date-fns-tz, Temporal.
- For scheduling, always include the timezone explicitly.
Timezone detection from IP is one of those small features that improves UX measurably. Users seeing dates in their local timezone is invisible-when-right, jarring-when-wrong. The Ip2Geo API returns IANA timezone with every lookup, making this a one-call enhancement. For related geographic personalization, see geo personalization; for the broader country detection, detect country from IP.