Humanize Duration: milliseconds to words, with the caveats written down
361000 becomes "6 minutes, 1 second"
At a glance
- What is it?
- A single-file JavaScript library that turns a millisecond count into a phrase like "1 day, 3 hours, 2 minutes", with per-language separators, digit substitution and a humanizer factory. Its own README tells new projects to use Intl.DurationFormat instead.
- Who is it for?
- Keep humanize-duration where it is already installed, because the language tables and the humanizer factory already do the work, and its last push was on 2026-10-14. Do not start new work on it: the README states no new features will be added and points at Intl.DurationFormat instead.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The author now recommends you not use it
The README opens the discussion of alternatives before the installation instructions. It suggests considering Intl.DurationFormat, the built-in web platform API, and it states plainly that the library is actively maintained but no new features will be added.
That combination is the most important thing to know. The last push was on 2026-10-14 and the repository is not archived. But the surface is frozen, and the README points to Intl.DurationFormat as an alternative.
The stated use case is narrow and specific: you have a duration in milliseconds and you want "30 minutes" or "3 days, 1 hour". If that is all you need on a modern runtime, the README suggests considering Intl.DurationFormat. This library offers the customization options covered below.
Installation is one line, and the file is usable without a bundler
The package is published as humanize-duration on npm and on Bower, and the JavaScript file can also be dropped into a page directly. The install command is the obvious one.
npm install humanize-durationUsage is a CommonJS require in Node or under a common build system.
const humanizeDuration = require("humanize-duration");
humanizeDuration(12000);
// => "12 seconds"For a plain page there is no build step at all: include the script and call the global.
<script src="humanize-duration.js"></script>The repository is consistent with that shape. humanize-duration.js sits at the root as a single file, with bower.json, a package-lock.json, an eslint.config.mjs, a tsconfig.json, a .prettierrc.json and a test/ directory. HISTORY.md tracks changes. The licence is the Unlicense, which places the code in the public domain and is about as permissive as a licence can get.
Default output keeps a decimal on the smallest unit
By default the library humanizes down to the second and returns a decimal for the smallest unit rather than rounding. Three examples from the README show the shape of the output.
humanizeDuration(3000);
// => "3 seconds"
humanizeDuration(2250);
// => "2.25 seconds"
humanizeDuration(97320000);
// => "1 day, 3 hours, 2 minutes"Zero-valued units are skipped rather than printed, which is why you do not get "0 minutes" in the last example.
That decimal behaviour has a consequence worth checking against your locale. The digitReplacements section shows humanizeDuration(1234) returning "1.234 seconds": the dot is the decimal separator in that output. In a locale where the decimal separator is a comma, a reader may parse it as a thousands separator. The round option is the switch that changes this, rounding the smallest unit instead.
humanizeDuration(1200);
// => "1.2 seconds"
humanizeDuration(1200, { round: true });
// => "1 second"If you want truncation to a fixed number of decimal places rather than rounding, maxDecimalPoints does that, and the README is explicit that it does not round any numbers.
units and largest are the two options that change the shape of the answer
units restricts which units appear and is an array of the keys y, mo, w, d, h, m, s and ms. Two constraints apply. A unit whose count is zero is skipped, and the array must be in descending order of unit size, so ["h", "m"] is valid and ["m", "h"] is not.
The order requirement is the one that bites. It exists because the library divides a running total in sequence, so an unsorted array produces a wrong answer rather than an error.
humanizeDuration(3600000, { units: ["h"] });
// => "1 hour"
humanizeDuration(3600000, { units: ["m"] });
// => "60 minutes"
humanizeDuration(3600000, { units: ["d", "h"] });
// => "1 hour"largest caps how many units appear, and defaults to Infinity.
humanizeDuration(1000000000000);
// => "31 years, 8 months, 1 week, 19 hours, 46 minutes, 40 seconds"
humanizeDuration(1000000000000, { largest: 2 });
// => "31 years, 8 months"That default is why a large millisecond value produces a six-part sentence rather than something readable, and largest is the option that fixes it.
Language is ISO 639-1, with an ordered fallback list
The language option takes an ISO 639-1 code from the supported list and defaults to en.
humanizeDuration(3000, { language: "es" });
// => "3 segundos"
humanizeDuration(5000, { language: "ko" });
// => "5 초"fallbacks is an array of further codes, evaluated left to right, and the first one found is used. It defaults to an empty array, which means an unknown language produces untranslated behaviour rather than an error.
humanizeDuration(3000, {
language: "bad language",
fallbacks: ["en"],
});
// => "3 seconds"The example passes a human-readable string rather than a code, and uses an English fallback.
The per-language defaults are deeper than word lists. Arabic gets its own default delimiter, and digitReplacements defaults to the Arabic-Indic numerals for that language while being undefined for most others. Setting digitReplacements to an array of ten strings replaces 0 through 9, which is how a language with different numerals is supported at all.
Separator options are separate because scripts differ, not just languages
The punctuation is configurable in three independent places, which is the clearest sign that this library is not simply a word list. delimiter goes between units and defaults to a comma and space in most languages, with a different character for Arabic. spacer goes between the count and the word and defaults to a single space. decimal goes between the integer and decimal parts of a count and its default depends on the language.
conjunction is inserted before the final unit, and serialComma set to false removes the comma that precedes it.
humanizeDuration(22141000, { conjunction: " and " });
// => "6 hours, 9 minutes, and 1 second"
humanizeDuration(22141000, { conjunction: " and ", serialComma: false });
// => "6 hours, 9 minutes and 1 second"The combination of conjunction and serialComma produces the Oxford comma, and turning it off produces the other convention. Neither is more correct; they are different languages' habits, which is exactly why they are exposed rather than hardcoded.
The spacer option is the one that looks silly and is occasionally necessary, since some scripts want a visible separator between numeral and word.
unitMeasures is where the ambiguous units are pinned down
A year and a month have no fixed length, so the library has to assume something. The default table is given in milliseconds: y at 31557600000, mo at 2629800000, w at 604800000, d at 86400000, h at 3600000, m at 60000, s at 1000 and ms at 1.
A year here is the average Gregorian year, and a month is derived from it, so 2629800000 milliseconds is one month. If you believe a month is thirty days, you can override it, and the result changes accordingly.
humanizeDuration(2629800000, {
unitMeasures: { mo: 30 * 86400000 },
});
// => "1 month, 10 hours, 30 minutes"The README marks this option as advanced and to be used with care, and the reason is that it interacts with the rest: every unit above the one you change is computed from the same table, so overriding mo changes what a year means in months as well.
It is also the option that makes the library usable for financial or reporting periods, where "a month" means something specific.
Humanizers let you stop repeating the same options
The last documented mechanism is the humanizer factory. Where you find yourself passing the same options repeatedly, you build a function once with changed defaults, which you can still override per call.
const spanishHumanizer = humanizeDuration.humanizer({
language: "es"
});The pattern is a bound configuration rather than a subclass. Nothing about the return type changes, so the humanizer is callable exactly like the base function and a component that receives one as a prop does not know whether it was configured.
That is the useful part of the design: the library's surface is one function plus one factory, and the documented options all funnel through the same second argument. The cost is that a heavily configured call site becomes hard to read, and the humanizer is the answer the author gives to that.
Editorial conclusion
Keep humanize-duration where it is already installed, because the language tables and the humanizer factory already do the work, and its last push was on 2026-10-14. Do not start new work on it: the README states no new features will be added and points at Intl.DurationFormat instead. If you do keep it, verify the units order before shipping, since the option must be given in descending unit size and an array like ["m", "h"] is invalid, and check the decimal behaviour for your locale, because 1234 milliseconds renders as "1.234 seconds" by default and that reads as a thousands separator in some locales.
Frequently asked questions
Should I still use humanize-duration on a new project?
The README suggests considering Intl.DurationFormat instead, and states that the library is actively maintained but that no new features will be added. The last push was on 2026-10-14.
How do I limit humanize-duration to two units?
Use the largest option, which defaults to Infinity. Passing { largest: 2 } on a large duration returns "31 years, 8 months" instead of the full six-part phrase. The units option can also restrict which units appear at all.
What is wrong with the order of the units array in humanize-duration?
The units array must be in descending order of unit size, so ["h", "m"] is valid and ["m", "h"] is not. Units with a zero count are skipped rather than printed.
What licence is humanize-duration released under?
The Unlicense, which places the code in the public domain. The package is available on npm as humanize-duration and on Bower, and the JavaScript file can also be included directly with a script tag.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/evanhahn-humanizeduration-js)Was this useful?