The unreasonable effectiveness of a changelog
A file that takes ten minutes per release and answers most of the questions your users would otherwise ask you.
Most projects do not have a changelog. Most projects have a commit log and a release page auto-generated from pull request titles, which is not the same thing and does not serve the same purpose.
A real changelog is a small amount of work with an outsized return.
what it is for#
Deciding whether to upgrade. The single most common reason someone reads a changelog. They are on version 3.2, version 3.7 exists, and they want to know whether it is worth the risk.
Knowing what will break. The most important information you can provide, and the thing auto-generated release notes are worst at.
Debugging. "This started failing after we upgraded" — a good changelog turns that into "here is the change that caused it" in thirty seconds.
Finding out what exists. People discover features by reading changelogs. This is a real and underrated distribution channel for your own work.
why generated release notes are not enough#
A list of merged pull request titles has three problems.
It is written for the wrong audience. "Refactor connection handling" means something to the maintainer and nothing to the user. What changed for them?
It has no hierarchy. A breaking change and a typo fix appear as sibling bullets of equal weight.
It has no migration guidance. "Remove deprecated parse() method" tells you something broke. It does not tell you what to do about it.
the format#
Keep a Changelog is the established convention and it is good. The structure:
## [4.2.0] - 2026-07-22
### Breaking
- `Client.connect()` no longer accepts a positional timeout.
Pass `timeout=` as a keyword.
# before
client.connect(host, 30)
# after
client.connect(host, timeout=30)
### Added
- `Client.ping()` for health checks without a full round trip (#412)
- Support for Unix domain sockets via `unix://` URLs (#398)
### Fixed
- Connections leaked when the handshake timed out (#405).
If you saw file descriptor exhaustion under load, this was it.
### Deprecated
- `Client.legacy_mode` — will be removed in 5.0. Use `compatibility=`.
### Security
- Fixed a case where credentials could appear in debug logs (GHSA-xxxx-xxxx).
Affects 4.0.0–4.1.3. Rotate credentials if debug logging was enabled.the rules that make it useful#
Breaking changes first, always. That is what people are scanning for. Do not bury them under twelve feature bullets.
Include the migration. A breaking change without "do this instead" makes the reader open your source code. Two lines of before-and-after saves everyone time.
Describe the user-visible effect, not the implementation. Not "refactored the retry logic." Rather: "retries now use exponential backoff with jitter; if you relied on the previous fixed 1-second interval, set retry_delay=1.0."
Say who is affected. "If you use X, this changes for you. Otherwise nothing changes." Most readers can then stop reading, which is a service.
Link to the issue or pull request for anyone who wants detail. The changelog is a summary, not a substitute.
Date every release, in ISO format. Version numbers alone do not tell you whether you are two months or three years behind.
Write it as you go, not at release time. An Unreleased section at the top that each pull request adds to. Reconstructing a changelog from git history at release time is miserable and it is why changelogs get skipped.
the security section specifically#
If you fix a security issue, say so, with:
- Which versions are affected.
- What the impact is.
- Whether any action beyond upgrading is required.
That last one is the part that gets omitted and it is critical. "Upgrade to 4.2.0" is insufficient if credentials may have been exposed — the user also needs to rotate them, and they will not know unless you say so.
for internal projects#
The same file works for internal services, and the audience is your future self and the person who takes over the service.
The most valuable internal changelog entries are the ones that record a decision:
## 2026-07-14
- Switched from polling to webhooks for order status. Polling was
costing ~40 requests/second against the vendor's rate limit and
we were getting throttled during peaks.Six months later, when someone asks why there is webhook infrastructure, the answer is in one place.
the return on investment#
Ten minutes per release. In exchange:
- Fewer support questions.
- Faster upgrades by your users, which means fewer people on old versions you have to support.
- Fewer surprised users after a breaking change.
- A record you can search when debugging.
There are not many ten-minute tasks with that profile.
— Dom, July 22, 2026