moment-timezone 0/8

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.

IANA 2026dJavaScript
01

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.

Analogy firstInstantZoneMust know

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.
TermMeansExample
InstantOne exact point in time, zone free1791264600000
UTCThe master clock with no daylight saving2026-09-29T04:30:00Z
OffsetHow far a wall clock is from UTC right now+05:30, -04:00
ZoneA named region with its full offset historyAmerica/New_York
AbbreviationA short label for display onlyIST, EDT, CEST
DSTClocks moved forward in summer, back in winterNew York: EST to EDT
model.jsJS
// 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());
TerminalOutput
$ 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.

02

Creating in a zone

Build a moment that belongs to a place, parse strings strictly, and load stored timestamps back in.

moment.tzParsingStrict modeMust know

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.

moment.tz(str, zone)

Reads the string as wall time in that zone.

See the example
Wall timeCommon
moment.tz(str, fmt, true, zone)

Strict parse with a format. Bad input becomes invalid.

See the example
StrictSafe
moment.utc(str).tz(zone)

Reads the string as UTC, then shows it in the zone.

See the example
Instant firstAPIs
moment.tz(epochMs, zone)

Loads a stored number and shows it in the zone.

See the example
From DBNo ambiguity
.isValid()

False when parsing failed. Check it before using the value.

See the example
GuardAlways
moment.tz.setDefault(zone)

Makes plain moment() use this zone app wide.

See the example
GlobalUse carefully
create.jsJS
// 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"));
TerminalOutput
$ 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.

03

Converting zones

Move an instant between cities, or keep the clock reading and swap the city, and know which one you are doing.

.tz()clonekeepLocalTimeMust know

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.

Convertm.clone().tz("America/New_York")Same instant, different wall clock.
Relabelm.clone().tz("Europe/London", true)Same wall clock, different instant.
convert.jsJS
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]"));
TerminalOutput
$ 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.

MethodChanges the instantChanges the reading
.tz(zone)NoYes
.tz(zone, true)YesNo
.utc()NoYes, to UTC
.local()NoYes, to the machine zone
.utcOffset(n)NoYes, to a fixed offset
04

Formatting

Print dates for people and for machines with format tokens, offsets and zone labels.

format()Tokensz and Z

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.

format.jsJS
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"));
TerminalOutput
$ 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.

TokenOutputMeaning
YYYY MM DD2026 12 31Year, month, day
ddd ddddThu ThursdayWeekday short and long
D MMM MMMM31 Dec DecemberDay and month names
HH:mm / h:mm A23:45 / 11:45 PM24 and 12 hour time
Z / ZZ-08:00 / -0800Offset from UTC
z / zzPSTZone abbreviation, display only
W / DDDD53 / 365ISO week, day of year
LT LL LLL LLLLLocale presetsTime, date, both, with weekday
[text]textEscaped literal
05

Daylight saving

Handle the night an hour vanishes and the night an hour happens twice, and know when a day is not 24 hours.

DSTGapsOverlapsMust know

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.

dst.jsJS
// 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());
TerminalOutput
$ 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.

SituationWhat moment doesWhat you should do
Adding days across a changeKeeps the wall clock, day length changesUse days for human schedules
Adding hours across a changeAdds exact time, wall clock shiftsUse hours for durations
Time in the spring gapMoves it forward past the gapValidate user entered times
Time in the autumn overlapTakes the earlier readingStore the instant, not the reading
Checking the season.isDST()Useful for labels, not for math
06

Zone data

Look up zones by name or country, read offsets straight from the database, and keep that data current.

moment.tz.zonenames()guess()

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.

moment.tz.zone(name)

The zone record, or null when the name is unknown.

See the example
LookupNull safe
zone.abbr(timestamp)

The abbreviation in force at that instant.

See the example
DisplayPer instant
zone.utcOffset(timestamp)

Minutes west of UTC, the reverse sign of a moment.

See the example
OffsetSign flip
moment.tz.names()

Every zone and alias name in the loaded data.

See the example
ListDropdowns
moment.tz.zonesForCountry(code)

Zone names for an ISO country code like IN or US.

See the example
CountryPickers
moment.tz.guess()

The browser or server zone, from the Intl API.

See the example
DefaultNot authority
zones.jsJS
const 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"));
TerminalOutput
$ 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.

07

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.

startOfdiffScheduling

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.

schedule.jsJS
// 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"));
TerminalOutput
$ 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.

QuestionWrite
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 timem.clone().tz(zone).hour(9).minute(0).second(0)
Store for the job queuenext.toISOString()
08

Pitfalls and alternatives

Dodge the mutation trap, then decide whether a new project should use Moment at all.

MutabilityProject statusIntlMust know

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.

pitfalls.jsJS
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")));
TerminalOutput
$ 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.

Needmoment-timezoneWithout Moment
Format in a zonem.tz(z).format("LLL")new Intl.DateTimeFormat(l, { timeZone: z })
Convert zonem.clone().tz(z)Luxon: dt.setZone(z)
Wall time in a zonemoment.tz(str, z)Luxon: DateTime.fromISO(str, { zone: z })
Add days safelym.clone().add(1, "day")Luxon: dt.plus({ days: 1 })
ImmutabilityClone by handBuilt in for Luxon and date-fns
Bundle sizeMoment plus zone dataIntl is free; others tree shake
Go

Which one do I need?

Find your situation, take the tool in the middle column and copy the starting line.

SituationReach forStart with
Store a timestamp in the databaseUTC instantm.toISOString() or m.valueOf()
Show a time to a userConvert, then formatmoment.utc(ts).tz(userZone).format("LLL z")
User typed a local date and timeParse in their zonemoment.tz(str, fmt, true, userZone)
Same time every day for a userAdd days in their zonem.clone().tz(zone).add(1, "day")
Token or session expiryAdd hours or minutesm.clone().add(15, "minutes")
Validate a zone from a formZone lookupmoment.tz.zone(input) !== null
Guess a default zoneBrowser guessmoment.tz.guess()
New project, formatting onlyNo libraryIntl.DateTimeFormat with timeZone

Credits

  • AuthorShree Kumar Sharma
  • DepartmentBackend Engineering
  • Co-AuthorClaude Design