Release Channels and Dist-Tags
Every npm install your-lib without a version installs whatever the latest dist-tag points to — so dist-tags, not version numbers, decide what most users get. Release channels built on dist-tags let you ship prereleases to early adopters (next, beta, rc), publish per-pull-request builds for testing (canary), and keep patching old major versions (v3-lts) without any of that reaching users who just want the stable release. Misusing them — a release candidate tagged latest, a backport that moves latest backwards — breaks installs across your user base in minutes, and the damage spreads through every lockfile regenerated while the mistake is live. This section explains how dist-tags work, how to design channels, and how to automate them safely.
How dist-tags work
A dist-tag is a named pointer from a label to one published version, stored in the package's registry metadata:
npm view your-lib dist-tags
# { latest: '3.8.2', next: '4.0.0-rc.3', canary: '4.0.0-canary.1f3c2a9', 'v2-lts': '2.19.4' }
npm install your-libinstallslatest.npm install your-lib@nextinstalls whatevernextpoints to.- Ranges such as
^3.8.0ignore tags entirely and pick the highest matching version — but they exclude prereleases unless the range itself names one.
Tags are mutable: npm dist-tag add your-lib@3.8.3 latest moves the pointer instantly, without publishing anything. Versions are immutable. That split — immutable versions, movable pointers — is what makes channels possible. The wider release process is covered in Package Publishing & Release Engineering.
Concept overview: designing channels
Most projects need only a few channels:
| Channel | Tag | Versions | Audience |
|---|---|---|---|
| Stable | latest |
3.8.2 |
everyone |
| Next major | next or rc |
4.0.0-rc.3 |
early adopters, framework partners |
| Experimental | canary or alpha |
4.0.0-canary.<sha> |
testing a specific change |
| Maintenance | v3-lts |
3.x patches |
users who cannot upgrade yet |
The guides in this section implement each one: Publishing Prereleases with Changesets Pre Mode for next, Publishing Canary Releases from Pull Requests for per-change builds, Maintaining LTS Branches with Backport Releases for maintenance lines, and Fixing a Wrong latest Dist-Tag for recovery when a tag points at the wrong version.
Core initialisation and configuration
Publishing to a channel is one flag:
npm publish --tag next # 4.0.0-rc.1 goes to next, latest is untouched
npm publish --tag canary # experimental builds
npm publish # stable: implicitly --tag latest
Recent npm 11 releases refuse to publish a prerelease version without an explicit --tag, precisely to stop release candidates from becoming latest. Older npm versions will happily do it, so configure the tag explicitly in CI regardless of the npm version.
Moving tags after publishing:
npm dist-tag add your-lib@4.0.0 latest # promote
npm dist-tag add your-lib@3.8.2 v3-lts # label the last 3.x
npm dist-tag rm your-lib canary # retire a channel
npm dist-tag ls your-lib # inspect
For packages published by release tools, set the tag in configuration rather than on the command line. In Changesets, pre mode sets it automatically; for manual control, changeset publish --tag next. In semantic-release, each branch in branches can declare a channel:
{
"branches": [
"main",
{ "name": "next", "channel": "next", "prerelease": "rc" },
{ "name": "3.x", "range": "3.x", "channel": "v3-lts" }
]
}
Architecture: how installs interact with tags and prereleases
Understanding what consumers' installs actually do prevents most channel mistakes:
- Fresh
npm install your-librecords a caret range of thelatestversion,^3.8.2. Movinglatestto4.0.0changes what new installs get, but existing^3.8.2ranges never jump to 4.x. - Ranges exclude prereleases.
^4.0.0-rc.1matches later release candidates and4.0.0, but^3.8.2never matches4.0.0-rc.3. Prereleases reach only users who opt in. - Moving
latestbackwards — pointing it at an older version after publishing a newer one — is allowed and sometimes necessary for recovery, but range resolution still sees the newer version:^3.8.2resolves to3.9.0if it exists, regardless of wherelatestpoints. - Lockfiles are unaffected by tag moves; they pin exact versions.
Channels in monorepos
In a monorepo that publishes many packages, channels need a consistent policy across packages, or consumers end up with mismatched prereleases. Two approaches work.
Channel per release, applied to every changed package. When the repository enters a prerelease cycle, every package released during the cycle goes to the same tag (next), so npm install @acme/react@next @acme/core@next gives a coherent set. Changesets' pre mode implements exactly this: while in pre mode, every published package gets a prerelease version and the pre tag. For fixed version groups this is natural; for independent packages it means some packages have prereleases and others do not, and consumers must understand that @next of an unchanged package simply points at its latest stable version or does not exist.
Channel per package. Packages with independent lifecycles run their own channels — @acme/cli may have a next while @acme/ui does not. This suits loosely related packages but makes "install the next versions of everything" impossible.
Whichever you choose, write down which tags exist for which packages, and check tag consistency in the release job with npm dist-tag ls for each published package.
Testing a channel before promotion
The point of a prerelease channel is feedback before latest moves, so plan how that feedback arrives. Useful sources are your own applications (upgrade one internal application to @next for each release candidate), a small group of external early adopters who have agreed to test release candidates, and automated compatibility checks — for example, a CI job in a few representative consumer projects that installs @next nightly and runs their test suites. Set an explicit promotion rule: for instance, a release candidate is promoted when it has been on next for a week, passed the consumer test jobs, and no blocking issue was reported. Written rules make promotion a routine decision rather than a judgement call under deadline pressure.
Execution strategy: automating channels safely
Channels are only as safe as the automation that publishes to them. Three rules cover most failures:
- Derive the tag from the version, never from memory. In CI, a prerelease version must map to its channel tag, and only stable versions from the stable branch may use
latest. - Publish canaries under unique prerelease versions, such as
0.0.0-canary-<sha>or4.0.0-canary.<sha>, so they can never be mistaken for or collide with real releases. - Promote by moving tags, not by republishing. A release candidate that passed testing becomes stable by publishing the final version — or, for projects that ship identical builds, by
npm dist-tag add your-lib@4.0.0 lateston an already-published final version.
A guard step in the release workflow enforces the first rule:
version=$(node -p "require('./package.json').version")
branch="${GITHUB_REF_NAME}"
if [[ "$version" == *-* ]]; then
tag="next"; [[ "$version" == *canary* ]] && tag="canary"
elif [[ "$branch" == "main" ]]; then
tag="latest"
else
tag="v${version%%.*}-lts"
fi
echo "Publishing $version with tag $tag"
npm publish --tag "$tag"
Retiring channels and cleaning up tags
Tags outlive their purpose unless someone removes them. After a major release, next still points at the last release candidate until the next cycle begins — anyone installing @next gets an outdated prerelease. After a maintenance line reaches end of life, its LTS tag keeps pointing at the last patch, which is fine as a record but should be announced as unsupported. Canary tags are the worst offenders, because they point at arbitrary experimental builds.
A short checklist after each major release keeps things tidy: move next to the new stable version (or remove it until the next cycle), create the LTS tag for the previous major, remove experiment-specific tags, and deprecate prerelease versions that should no longer be installed. Deprecation is covered in Deprecating npm Package Versions.
Communicating channels to users
Most users never look at dist-tags, so channels only help if they are documented where users look. Put a short "Release channels" section in the README listing each tag, what it contains, and how stable it is. Mention the channel in every prerelease's release notes and in the install command you share (npm install your-lib@next). For LTS lines, state the support window — "3.x receives security fixes until 2027-03-31" — and link to the upgrade guide for the current major. When latest moves to a new major, announce it with migration notes; many users only discover a major version when a fresh install gives them one.
Registry behaviour and caching
Tag changes take effect on the registry immediately, but clients see them through caches. npm caches package metadata locally and respects cache headers from the registry and any CDN in between, so a user who installed a minute before you moved latest may get the old target for a short while. Proxy registries (Verdaccio, Artifactory, CodeArtifact) cache metadata too, often for longer, and serve their cached view of tags to everyone behind them. Two consequences follow. First, after a tag fix, users behind a proxy can keep receiving the wrong version until the proxy refreshes; tell platform teams to refresh metadata for the package, or wait out the cache window. Second, automation that publishes and immediately installs by tag — for example, a documentation site that installs @next right after a prerelease is published — should install by exact version instead, or poll until the registry reports the new tag, as described in Fixing npm ETARGET 'No Matching Version Found'.
Channels for applications versus libraries
Dist-tags are a library concern: they decide what consumers install. Applications that are deployed rather than installed rarely need them. When an organisation publishes internal applications as packages — CLIs distributed through a private registry, or deployable bundles — channels work the same way and give operations teams a clean way to roll out: publish 2.4.0-rc.1 to next, let a pilot group install it, then promote. For web applications deployed from a repository, the equivalent is deployment environments and feature flags, not dist-tags.
Channel design checklist
Before adding a new channel, answer four questions: who installs it, how it is published (which branch and workflow), what version scheme it uses (so it never collides with stable versions), and when it will be retired. If any answer is unclear, the channel will probably confuse users more than it helps. A small set of well-documented channels — latest, next, and one LTS line — serves most packages; add canary only if you actively test per-change builds with real consumers.
Security and isolation
Channels multiply the number of published versions and the number of workflows that publish, so apply the same controls to all of them. Canary and prerelease workflows should use trusted publishing or tokens as tightly scoped as stable releases, because a canary is installable by anyone who asks for it. Pull requests from forks must not publish canaries — they would run untrusted code with publish rights — so restrict canary publishing to branches in your own repository or to a maintainer-triggered workflow. Dist-tag changes are writes that require the same authentication as publishing, and with 2FA-required packages they need either an interactive OTP or the trusted CI identity. Treat latest as a production deployment: only a reviewed workflow on a protected branch should move it.
CI/CD integration
A single workflow can serve stable, prerelease and maintenance channels by branch:
name: release
on:
push:
branches: [main, next, "v*.x"]
permissions:
contents: write
id-token: write # trusted publishing + provenance
jobs:
publish:
runs-on: ubuntu-latest
environment: npm
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: 24, registry-url: https://registry.npmjs.org }
- run: npm ci && npm run build && npm test
- name: Compute dist-tag from branch and version
id: tag
run: |
v=$(node -p "require('./package.json').version")
case "$GITHUB_REF_NAME" in
main) [[ "$v" == *-* ]] && { echo "prerelease on main"; exit 1; }; t=latest ;;
next) t=next ;;
v*.x) t="${GITHUB_REF_NAME%.x}-lts" ;;
esac
echo "tag=$t" >> "$GITHUB_OUTPUT"
- run: npm publish --tag "${{ steps.tag.outputs.tag }}"
- run: npm dist-tag ls "$(node -p "require('./package.json').name")"
Step by step: each branch maps to exactly one channel; a prerelease version on main fails the job; publishing uses the computed tag; the final step prints the resulting tags into the log so any surprise is visible immediately.
Worked example: introducing channels to a mature library
A widely used date library had only ever published to latest. Its maintainers wanted to ship a major rewrite without surprising the thousands of projects that install it without a version. They introduced three channels in one release cycle. First, every merged pull request on the v5 branch published a canary (5.0.0-canary.<sha>) so contributors could try changes in real projects. Second, feature-complete builds were published as release candidates under next, and the README gained a "Try the next major" section with npm install date-lib@next. Third, when 5.0.0 shipped, latest moved to it, the last 4.x release was tagged v4-lts, and the README stated that 4.x would receive security fixes for twelve months.
The CI workflow computed tags from branches and versions, so no release could accidentally land on latest, and a scheduled job reported every tag's target weekly. Over the release candidate period, three consumer projects run by the maintainers' employer installed @next nightly and caught two regressions before the stable release. When 5.1.0 later needed a fix backported to 4.x, the maintenance workflow published 4.12.3 under v4-lts without touching latest.
Pitfalls
| Mistake | Impact | Remediation |
|---|---|---|
Publishing a prerelease without --tag |
RC becomes latest for every new install |
Derive tag from version; use npm 11+ |
Backport release published as latest |
latest moves backwards to an old major |
Publish maintenance releases with an LTS tag |
| Canary versions that look like real releases | Users pin experimental builds | Unique prerelease identifiers with a SHA |
| Forks can trigger canary publishing | Untrusted code published under your name | Restrict to same-repository branches |
| Stale tags left behind | Users install abandoned prereleases via @next |
Retire or move tags after each major |
Guides in this topic
Every guide below solves one concrete task or error within Release Channels and Dist-Tags. Start with the one whose symptom matches what you are seeing:
- Fixing a Wrong latest Dist-Tag — When latest points at the wrong version, every new npm install your-lib gets that version: a release candidate, a canary build, or an old maintenance release that moved…
- Maintaining LTS Branches with Backport Releases — After a major release, many users cannot upgrade immediately: the migration takes work, their framework version lags behind, or their own release cycle is slow.
- Publishing Canary Releases from Pull Requests — Reviewing a library change by reading the diff only goes so far; the real test is installing it into the application that needs it.
- Publishing Prereleases with Changesets Pre Mode — Changesets normally turns accumulated changeset files into stable version bumps.
Frequently Asked Questions
Can two tags point to the same version?
Yes. After promoting 4.0.0, both latest and next may point to it until the next prerelease cycle starts; that is normal.
Can I delete the latest tag?
No — every package must have a latest tag. You can move it to another version, but not remove it.
Do dist-tags affect GitHub Packages and private registries? Most npm-compatible registries support dist-tags with the same commands. Check your registry's documentation for tag permissions and limits.
How do consumers find out about a channel?
Document channels in the README (npm install your-lib@next for the upcoming major) and announce prereleases in release notes. Tags are discoverable with npm view your-lib dist-tags, but few users look.
Why did npm install give some users a release candidate?
Almost always because a prerelease was published without --tag and became latest. Move latest back to the stable version immediately, as described in Fixing a Wrong latest Dist-Tag, then fix the workflow.
Is there a limit on the number of dist-tags? There is no practical limit for normal use, but every tag is something users might install. Keep tags few and meaningful, and remove experimental ones when they are no longer needed.
Can I publish to a channel from a local machine? You can, with 2FA, but channel publishes are easy to get wrong by hand. Route every channel through the same reviewed workflow as stable releases, and keep manual publishing for emergencies with a dry run first.
Related
- Package Publishing & Release Engineering covers the whole release process.
- Semantic Versioning and Release Automation explains how versions are chosen before they are tagged.
- npm Registry Publishing Workflows describes publishing and authentication.
- Package Deprecation and Lifecycle covers retiring versions and channels at the end of their life.