Backend engineeringDates and time zonesmoment-timezone 0.6
Timezone
Toolbox
An instant is one tick of the world clock; a time zone is the wall clock each city hangs over it. moment-timezone lets you read any instant on any city's clock, through daylight saving and back. Every rule you reach for daily is here and runs in the page.
The mental model
An instant is one tick of the world clock. A time zone is the wall clock a city hangs over it. Keep those two ideas apart and most date bugs disappear.
Picture the departures board at an international airport. The master clock in the control room ticks once for the whole planet: that is an instant, and computers store it as milliseconds since 1970 in UTC. Every gate has its own wall clock showing that same tick in a local style: that is a time zone. A flight leaves at one instant, yet Delhi reads 10:00 while London reads 05:30.
moment-timezone adds that wall clock to Moment. It ships the IANA time zone database, the official record of every region's offsets and daylight saving rules, and lets you ask moment.tz(...) to show any instant in any zone by its name, such as Asia/Kolkata. Offsets like +05:30 are the reading on the wall clock today; the zone name is the rulebook that says what the reading will be on any date.
Picture it: one instant, three wall clocks
04:30 UTC.tz(zone)→10:00 IST05:30 BST00:30 EDTThe epoch value is identical in all three. Only the reading changes.| Term | Means | Example |
|---|---|---|
| Instant | One exact point in time, zone free | 1791264600000 |
| UTC | The master clock with no daylight saving | 2026-09-29T04:30:00Z |
| Offset | How far a wall clock is from UTC right now | +05:30, -04:00 |
| Zone | A named region with its full offset history | America/New_York |
| Abbreviation | A short label for display only | IST, EDT, CEST |
| DST | Clocks moved forward in summer, back in winter | New York: EST to EDT |
// One instant: the moment a deploy finished
const deploy = moment.utc("2026-09-29T04:30:00Z");
console.log(deploy.clone().tz("Asia/Kolkata").format("YYYY-MM-DD HH:mm z"));
console.log(deploy.clone().tz("Europe/London").format("YYYY-MM-DD HH:mm z"));
console.log(deploy.clone().tz("America/New_York").format("YYYY-MM-DD HH:mm z"));
// Same instant, so the epoch value never changes
console.log(deploy.valueOf() === deploy.clone().tz("Asia/Tokyo").valueOf());
$ node model.js 2026-09-29 10:00 IST 2026-09-29 05:30 BST 2026-09-29 00:30 EDT true
Why it matters: the same deploy reads as three different clock times, yet the epoch value is equal. Store the instant, and choose the zone only when a human needs to read it.
Creating in a zone
Build a moment that belongs to a place, parse strings strictly, and load stored timestamps back in.
moment.tz(input, zone) means "this wall clock reading, in that city". A standup at 09:30 in Kolkata is written with the Kolkata zone, and moment works out the instant behind it. Compare that with moment.utc(input).tz(zone), which means "this instant, now show it in that city". Getting the two backwards shifts every time by the offset.
Always pass a format when the input is not ISO 8601, and pass true for strict parsing so a wrong shape is rejected instead of guessed. Numbers are treated as epoch milliseconds, the safest thing to keep in a database.
Reads the string as wall time in that zone.
See the exampleStrict parse with a format. Bad input becomes invalid.
See the exampleReads the string as UTC, then shows it in the zone.
See the exampleLoads a stored number and shows it in the zone.
See the exampleFalse when parsing failed. Check it before using the value.
See the exampleMakes plain moment() use this zone app wide.
See the example// A wall clock time that belongs to a place
const standup = moment.tz("2026-10-05 09:30", "YYYY-MM-DD HH:mm", "Asia/Kolkata");
console.log(standup.format());
console.log(standup.toISOString());
// Strict parsing rejects anything that does not match the format
const bad = moment.tz("05/10/2026 9.30", "YYYY-MM-DD HH:mm", true, "Asia/Kolkata");
console.log(bad.isValid());
// From a stored epoch in milliseconds
console.log(moment.tz(1791264600000, "Asia/Kolkata").format("LLLL"));
$ node create.js 2026-10-05T09:30:00+05:30 2026-10-05T04:00:00.000Z false Tuesday, October 6, 2026 11:00 AM
Why it matters: .format() keeps the Kolkata offset while .toISOString() always prints UTC. The strict parse refuses a day first string instead of silently reading 5 October as May 10.
Converting zones
Move an instant between cities, or keep the clock reading and swap the city, and know which one you are doing.
Calling .tz(zone) on a moment converts it: same instant, new wall clock. It also mutates the object you call it on, so clone first whenever the original is still needed. Passing true as the second argument does the opposite trick: it keeps the reading, 19:00, and moves it to another city, which changes the instant. Use that when a user fixes a mistakenly chosen zone on a form.
.utcOffset() gives the offset in minutes east of UTC, so India reads 330 and New York in summer reads -240.
const call = moment.tz("2026-10-05 19:00", "Asia/Kolkata");
// .tz() changes the zone of this object in place, so clone first
const ny = call.clone().tz("America/New_York");
console.log(call.format("ddd HH:mm z"), "|", ny.format("ddd HH:mm z"));
// Shift the zone but keep the wall clock: 19:00 in London instead
const london = call.clone().tz("Europe/London", true);
console.log(london.format("HH:mm z"), london.toISOString());
console.log(call.utcOffset(), ny.utcOffset());
console.log(call.clone().utc().format("YYYY-MM-DD HH:mm [UTC]"));
$ node convert.js Mon 19:00 IST | Mon 09:30 EDT 19:00 BST 2026-10-05T18:00:00.000Z 330 -240 2026-10-05 13:30 UTC
Why it matters: a 19:00 call in India is 09:30 the same morning in New York. The relabelled London moment still reads 19:00, but its UTC value moved by four and a half hours.
| Method | Changes the instant | Changes the reading |
|---|---|---|
.tz(zone) | No | Yes |
.tz(zone, true) | Yes | No |
.utc() | No | Yes, to UTC |
.local() | No | Yes, to the machine zone |
.utcOffset(n) | No | Yes, to a fixed offset |
Formatting
Print dates for people and for machines with format tokens, offsets and zone labels.
Formatting is choosing how the wall clock is written on the board. Tokens stand in for parts of the date: YYYY is the year, HH a 24 hour hour, h A a 12 hour hour with AM or PM. Anything in square brackets is printed as is.
The zone tokens are where moment-timezone matters. Z prints the numeric offset, and z prints the abbreviation such as PST, which only works with a zone attached. Abbreviations are for display only: IST means India, Ireland and Israel depending on who you ask, so never store or parse them. For machines, send toISOString() or epoch milliseconds.
const t = moment.tz("2026-12-31 23:45", "America/Los_Angeles");
console.log(t.format("YYYY-MM-DD HH:mm"));
console.log(t.format("ddd, D MMM YYYY h:mm A"));
console.log(t.format("Z"), t.format("ZZ"), t.format("z"));
console.log(t.format("[Week] W, [day] DDDD"));
console.log(t.format("LLL"));
console.log(t.clone().tz("Asia/Kolkata").format("LLL z"));
$ node format.js 2026-12-31 23:45 Thu, 31 Dec 2026 11:45 PM -08:00 -0800 PST Week 53, day 365 December 31, 2026 11:45 PM January 1, 2027 1:15 PM IST
Why it matters: the last minutes of 2026 in Los Angeles are already the afternoon of New Year's Day in India. Localised tokens like LLL keep the date readable without writing the pattern by hand.
| Token | Output | Meaning |
|---|---|---|
YYYY MM DD | 2026 12 31 | Year, month, day |
ddd dddd | Thu Thursday | Weekday short and long |
D MMM MMMM | 31 Dec December | Day and month names |
HH:mm / h:mm A | 23:45 / 11:45 PM | 24 and 12 hour time |
Z / ZZ | -08:00 / -0800 | Offset from UTC |
z / zz | PST | Zone abbreviation, display only |
W / DDDD | 53 / 365 | ISO week, day of year |
LT LL LLL LLLL | Locale presets | Time, date, both, with weekday |
[text] | text | Escaped literal |
Daylight saving
Handle the night an hour vanishes and the night an hour happens twice, and know when a day is not 24 hours.
Twice a year, many wall clocks jump. In spring they skip forward an hour, so a slice of time never appears on the board; in autumn they fall back, so the same reading appears twice. India does not observe daylight saving, which is exactly why bugs slip past teams testing only in IST.
moment-timezone follows the zone rules for you. Adding 1, "day" keeps the wall clock time and lets the day be 23 hours long, while adding 24, "hours" adds exact elapsed time. A time inside the spring gap is moved forward, and an ambiguous autumn time takes the earlier of the two readings.
// US clocks spring forward on 8 March 2026 at 02:00
const before = moment.tz("2026-03-07 12:00", "America/New_York");
console.log(before.clone().add(1, "day").format("MMM D HH:mm z"));
console.log(before.clone().add(24, "hours").format("MMM D HH:mm z"));
// 02:30 does not exist that night, so moment moves it forward
console.log(moment.tz("2026-03-08 02:30", "America/New_York").format("HH:mm z"));
// 01:30 happens twice on 1 November; moment picks the first one
const twice = moment.tz("2026-11-01 01:30", "America/New_York");
console.log(twice.format("HH:mm z"), twice.isDST());
console.log(moment.tz("2026-07-01", "Asia/Kolkata").isDST());
$ node dst.js Mar 8 12:00 EDT Mar 8 13:00 EDT 03:30 EDT 01:30 EDT true false
Why it matters: "same time tomorrow" and "24 hours later" are different answers on a DST night. Pick days for calendar events such as reminders, and hours for elapsed time such as token expiry.
| Situation | What moment does | What you should do |
|---|---|---|
| Adding days across a change | Keeps the wall clock, day length changes | Use days for human schedules |
| Adding hours across a change | Adds exact time, wall clock shifts | Use hours for durations |
| Time in the spring gap | Moves it forward past the gap | Validate user entered times |
| Time in the autumn overlap | Takes the earlier reading | Store the instant, not the reading |
| Checking the season | .isDST() | Useful for labels, not for math |
Zone data
Look up zones by name or country, read offsets straight from the database, and keep that data current.
Behind every conversion is the data file, a timetable of every offset change each zone has ever had. moment.tz.zone(name) opens one entry so you can read its abbreviation or offset at any timestamp. Watch the sign: a zone object's utcOffset() returns minutes west of UTC, matching the old Date API, so Berlin in summer reads -120 while a moment in Berlin reads 120.
moment.tz.guess() asks the browser which zone it is in, which is handy for defaults on a signup form but should never override a zone the user picked. Governments change their rules several times a year, so updating the package is how new rules reach your app. The data also comes in smaller builds, such as a 1970 to 2030 range, to cut the bundle.
The zone record, or null when the name is unknown.
See the exampleThe abbreviation in force at that instant.
See the exampleMinutes west of UTC, the reverse sign of a moment.
See the exampleEvery zone and alias name in the loaded data.
See the exampleZone names for an ISO country code like IN or US.
See the exampleThe browser or server zone, from the Intl API.
See the exampleconst zone = moment.tz.zone("Europe/Berlin");
console.log(zone.name, zone.abbr(Date.UTC(2026, 6, 1)), zone.abbr(Date.UTC(2026, 0, 1)));
// utcOffset on a zone returns minutes WEST of UTC, the opposite sign
console.log(zone.utcOffset(Date.UTC(2026, 6, 1)));
console.log(moment.tz("2026-07-01", "Europe/Berlin").utcOffset());
console.log(moment.tz.zonesForCountry("IN"));
console.log(moment.tz.names().includes("Asia/Calcutta"), moment.tz.names().length > 500);
console.log(moment.tz.zone("Mars/Olympus"));
$ node zones.js Europe/Berlin CEST CET -120 120 [ 'Asia/Kolkata' ] true true null
Why it matters: looking up an unknown name returns null instead of throwing, so validate a zone from user input with moment.tz.zone(input) !== null. Old names such as Asia/Calcutta still work as aliases.
Math and scheduling
Answer "what is today" for a user, find the start of their day, and schedule the next 9 am in their zone.
The trickiest questions are about days, because a day starts at a different instant in every zone. At 02:30 UTC it is already the 29th on the server and still the 28th in New York. Convert into the user's zone first, then call startOf("day"), isSame(other, "day") or set the hour.
The pattern for reminders: take now, move it into the user's zone, set the local hour, roll to tomorrow if it has passed, then store the UTC result. The job runner only ever sees instants.
// A user in New York and a server that thinks in UTC
const now = moment.utc("2026-09-29T02:30:00Z");
const local = now.clone().tz("America/New_York");
// "Today" depends on where you stand
console.log(now.format("YYYY-MM-DD"), local.format("YYYY-MM-DD"));
const dayStart = local.clone().startOf("day");
console.log(dayStart.format(), dayStart.clone().utc().format());
// Next 9 am in the user's zone, stored as UTC
let next = local.clone().hour(9).minute(0).second(0);
if (next.isBefore(local)) next.add(1, "day");
console.log(next.format("ddd HH:mm z"), next.toISOString());
const a = moment.tz("2026-10-05 23:30", "Asia/Kolkata");
const b = moment.tz("2026-10-06 00:30", "Asia/Kolkata");
console.log(b.diff(a, "minutes"), a.isSame(b, "day"));
$ node schedule.js 2026-09-29 2026-09-28 2026-09-28T00:00:00-04:00 2026-09-28T04:00:00Z Tue 09:00 EDT 2026-09-29T13:00:00.000Z 60 false
Why it matters: the start of the New York day is 04:00 UTC, not midnight UTC. Two Kolkata times an hour apart are on different days because midnight sits between them.
| Question | Write |
|---|---|
| Is it the same calendar day for this user? | a.clone().tz(zone).isSame(b.clone().tz(zone), "day") |
| When does their day start? | m.clone().tz(zone).startOf("day") |
| How long between two instants? | b.diff(a, "minutes") |
| Next 9 am their time | m.clone().tz(zone).hour(9).minute(0).second(0) |
| Store for the job queue | next.toISOString() |
Pitfalls and alternatives
Dodge the mutation trap, then decide whether a new project should use Moment at all.
Moment objects are mutable: add, subtract, startOf and tz change the object and return it. In the example, start and end end up as the same object, both reading 12:00. The fix is a habit: .clone() before any change.
The Moment team describes the library as a legacy project in maintenance mode: it still gets security and data fixes, but no new features, and they suggest newer libraries for new work. moment-timezone keeps its data updated, so existing code is safe to run. For new projects, the built in Intl API covers formatting in any zone with no bundle cost, and libraries such as Luxon or date-fns with its time zone add-on cover the math.
const start = moment.tz("2026-10-05 10:00", "Asia/Kolkata");
// Moment objects mutate: this changes start itself
const end = start.add(2, "hours");
console.log(start.format("HH:mm"), end.format("HH:mm"), start === end);
// Clone before every change you do not want to leak
const s2 = moment.tz("2026-10-05 10:00", "Asia/Kolkata");
const e2 = s2.clone().add(2, "hours");
console.log(s2.format("HH:mm"), e2.format("HH:mm"));
// The same job with the built in Intl API, no library
const fmt = new Intl.DateTimeFormat("en-IN", { timeZone: "Asia/Kolkata", dateStyle: "medium", timeStyle: "short" });
console.log(fmt.format(new Date("2026-10-05T04:30:00Z")));
$ node pitfalls.js 12:00 12:00 true 10:00 12:00 5 Oct 2026, 10:00 am
Why it matters: the mutation bug is silent, since both variables print the same time. The last line shows that formatting in a named zone needs no library at all with Intl.DateTimeFormat.
| Need | moment-timezone | Without Moment |
|---|---|---|
| Format in a zone | m.tz(z).format("LLL") | new Intl.DateTimeFormat(l, { timeZone: z }) |
| Convert zone | m.clone().tz(z) | Luxon: dt.setZone(z) |
| Wall time in a zone | moment.tz(str, z) | Luxon: DateTime.fromISO(str, { zone: z }) |
| Add days safely | m.clone().add(1, "day") | Luxon: dt.plus({ days: 1 }) |
| Immutability | Clone by hand | Built in for Luxon and date-fns |
| Bundle size | Moment plus zone data | Intl is free; others tree shake |
Which one do I need?
Find your situation, take the tool in the middle column and copy the starting line.
| Situation | Reach for | Start with |
|---|---|---|
| Store a timestamp in the database | UTC instant | m.toISOString() or m.valueOf() |
| Show a time to a user | Convert, then format | moment.utc(ts).tz(userZone).format("LLL z") |
| User typed a local date and time | Parse in their zone | moment.tz(str, fmt, true, userZone) |
| Same time every day for a user | Add days in their zone | m.clone().tz(zone).add(1, "day") |
| Token or session expiry | Add hours or minutes | m.clone().add(15, "minutes") |
| Validate a zone from a form | Zone lookup | moment.tz.zone(input) !== null |
| Guess a default zone | Browser guess | moment.tz.guess() |
| New project, formatting only | No library | Intl.DateTimeFormat with timeZone |
Credits
- AuthorShree Kumar Sharma
- DepartmentBackend Engineering
- Co-AuthorClaude Design