# Semantic versioning

## What it is

Semantic versioning, usually shortened to SemVer, is a set of rules for
numbering releases so that the number itself says what kind of change a release
contains. The specification, now at version 2.0.0 and originally written by Tom
Preston-Werner, uses three numbers in the form MAJOR.MINOR.PATCH. A release that
breaks the public API raises the major number, one that adds functionality in a
backward-compatible way raises the minor number, and one that only fixes bugs
without touching the API raises the patch number. When a number goes up, the
numbers to its right go back to zero, and each is compared as a number rather
than as text, so `1.10.0` comes after `1.9.0`.

Everything rests on one requirement: the project must declare a public API, in
the code or in its documentation, because the numbers describe changes to that
API and nothing else. Once a version is released, its contents must not change;
a correction is a new version. Major version zero, `0.y.z`, is for initial
development, where anything may change at any time and the API should not be
considered stable. Version `1.0.0` is the point at which the public API is
defined, and from then on the numbers follow how it changes.

Two optional labels extend the format. A pre-release goes after a hyphen and
sorts before the release it leads up to, so `1.0.0-alpha` comes before
`1.0.0-alpha.1`, which comes before `1.0.0-beta`, `1.0.0-rc.1` and finally
`1.0.0`. Build metadata goes after a plus sign, as in `1.0.0+20130313144700`,
and is ignored when versions are put in order, so two versions that differ only
there rank the same. A leading v, as in the Git tag `v1.2.3`, is not part of the
semantic version; the specification calls it a common way of marking a version
number. Go makes it mandatory: a canonical Go module version is the letter v
followed by a semantic version.

## Why it matters

The specification starts from what it calls dependency hell. Specify
dependencies too tightly and you get version lock, where upgrading one package
means releasing new versions of everything that depends on it. Specify them too
loosely and you get what the specification calls version promiscuity: accepting
future releases without a sound basis for expecting compatibility. A version
number with an agreed meaning lets a project state what it will accept: a
library that needs a function first added in 3.1.0 can accept anything from
3.1.0 up to, but not including, 4.0.0, and take later fixes and additions
without anyone editing the requirement.

Package managers act on that promise without asking. In Cargo, the Rust package
manager, a plain requirement such as `"1.2.3"` is a caret requirement and means
at least 1.2.3 but below 2.0.0. When it writes `Cargo.lock`, Cargo picks the
greatest version inside that range, and it stays on the 1.x line after 2.0.0 is
published, because it treats 2.0.0 as incompatible. In node-semver, which calls
itself the semantic versioner for npm, `^1.2.3` means `>=1.2.3 <2.0.0-0` and
`~1.2.3` allows only patch-level changes, `>=1.2.3 <1.3.0-0`. Go builds the rule
into import paths: from major version 2 onward a module's path must end in a
suffix such as `/v2`, so two incompatible major versions are two different
import paths. Go's minimal version selection then takes, for each module, the
highest version that any requirement asks for, which is the minimum that
satisfies all of them. The result is not saved in a lock file; every
module-aware command computes it again.

None of this makes the number a guarantee. It is a promise made by the people
who publish the package, and it is only as good as their judgement about what
counts as breaking, which is why Cargo publishes a long compatibility guide that
sorts changes into major, minor and possibly-breaking. Most of that guide is
about changes that stop code that compiled yesterday from compiling today. The
guide itself notes that almost every change carries some risk to runtime
behaviour, and that whether such a change is incompatible is usually a judgement
call for the maintainers.

## What it looks like in practice

Follow an invented Rust library, tally, through its releases. Version 0.4.2 is
still initial development. Version 1.0.0 declares the public API stable. Version
1.0.1 fixes a rounding bug and is a patch. Version 1.1.0 adds a function,
`parse_strict`, and marks the old `parse` as deprecated: the specification
requires a minor release for a deprecation and asks for at least one such
release before the function is removed. Version 2.0.0 removes `parse` and is a
major release. Version 2.1.0-rc.1 is a pre-release of the next minor version.

An application that depends on tally and three other invented crates states its
requirements in `Cargo.toml`. The comments give the range Cargo reads from each
line:

```toml
[dependencies]
tally = "1.0.1"         # >=1.0.1, <2.0.0: takes 1.1.0, never 2.0.0
ledger = "0.4.2"        # >=0.4.2, <0.5.0: 0.5.0 counts as breaking
stamp = "~1.2.3"        # >=1.2.3, <1.3.0: patch releases only
quill = "=2.1.0-rc.1"   # exactly this pre-release
```

The `ledger` line shows a convention of the tools rather than of the
specification: Cargo treats the left-most non-zero number as the one that
signals a break, where the specification says everything below 1.0.0 may change
at any time. Cargo leaves pre-releases out of a requirement unless it names one,
so a requirement of `"2.0"` would never pick up tally's release candidate; the
exact `=` form on the last line is the case where Cargo's documentation says a
fully specified requirement might be warranted: depending on a pre-release while
avoiding breaking changes between pre-releases. Once resolved, the versions are
recorded in `Cargo.lock`, and `cargo update` moves them within these ranges.
With `--locked`, a build that would have to change the lock file returns an
error instead.

What the number does not cover is anything outside the API. The
[licence audit of 644 Rust crates](/portfolio/know-the-licence-of-everything-you-ship/),
carried out in September 2026, checks the licence of every crate on every build
against an allow list, because a routine version bump deep in the dependency
graph can change what an application may be distributed under, silently, in a
lock file that nobody reads in full. To prove the check works, it was made to
fail twice on purpose. One test rewrote a crate's licence expression, the shape
of a crate changing its terms upstream between versions, which human review
misses because the crate itself is not new. The other moved the pinned core
revision without regenerating the audit. A semantic version can tell you that
1.4.0 is meant to work with code written against 1.0.0. It cannot tell you that
you may still ship it.

## Where it goes wrong

**A breaking change in a minor release.** It happens by accident or through a
different view of what counts as breaking. Cargo's guide lists changes that look
harmless and are not: adding a variant to a public enum breaks code that matches
on it exhaustively, unless the enum was marked `#[non_exhaustive]` from the
start, and in rare cases involving glob imports even adding a new public item
causes a compile error. The specification's answer, once the mistake is noticed,
is to release a new minor version that restores compatibility, never to modify
the release already published, and to tell users which version was at fault. On
the consuming side, a committed lock file and tests that run on every update are
what catch it before production does.

**Staying at 0.x for years.** Under the specification, major version zero
carries no stable API at all. Its FAQ is blunt: "If your software is being used
in production, it should probably already be 1.0.0." The tools fill the gap with
their own convention: Cargo and node-semver treat the left-most non-zero number
as the breaking one, so `^0.2.3` accepts only further 0.2.x releases and
`^0.0.3` accepts no updates at all. A library that has sat at 0.x for years has
users who depend on it but no defined promise to them. Release 1.0.0 once people
rely on the API; it changes a number, not the code.

**Reading more into the number than it says.** Compatibility under the
specification is about the public API. Cargo's guide classes raising the minimum
Rust version a crate needs as possibly-breaking and generally recommends
treating it as a minor change, so a minor update can stop the build on an older
toolchain. Setting `package.rust-version` lets Cargo's resolver try older
versions of the crate when needed. Licence terms, performance and behaviour the
API never documented are outside the number altogether. Check them separately,
with a licence audit and tests, rather than inferring them from a minor release.

**Two major versions in one build.** When two dependencies need incompatible
versions of the same crate, say 0.6 and 0.7, Cargo builds both. The compiler
treats their types as different even when the names match. That usually shows up
as a compile error and sometimes only at runtime: Cargo's documentation
describes a downcast that fails because the object was created by a library
using version 1.0.0 while the calling code downcasts to the type from 2.0.0.
`cargo tree -d` lists duplicate versions and where they come from. A library
author can soften a major release with what Cargo's documentation calls the
semver trick: publish the new major version, then a point release of the old one
that re-exports the types from the new.

## Further reading

- [Semantic Versioning 2.0.0](https://semver.org/)
  — the specification and its FAQ, including what to do after releasing a
  breaking change by mistake.
- [SemVer Compatibility](https://doc.rust-lang.org/cargo/reference/semver.html)
  — the Cargo Book's guide to which changes in a Rust library are major, minor
  or possibly-breaking, each with an example.
- [Specifying Dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)
  — Cargo's requirement syntax: default, caret, tilde and comparison
  requirements, and how pre-releases are matched.
- [Dependency Resolution](https://doc.rust-lang.org/cargo/reference/resolver.html)
  — how Cargo picks versions, what the lock file holds and the hazards of two
  incompatible versions in one graph.
- [node-semver](https://github.com/npm/node-semver) — the semantic versioner
  for npm, with its caret, tilde and pre-release range rules.
- [Go Modules Reference](https://go.dev/ref/mod) — major version suffixes, the
  import compatibility rule and minimal version selection.

<https://engineer.company/glossary/semantic-versioning/>
