Comments matter because code is read far more often than it is written, and the parts a reader cannot reconstruct from the syntax—intent, constraints, units, failure modes, and the reasons behind non-obvious choices—are exactly the parts that comments must carry. A comment earns its place when it records information the code cannot express on its own, and it becomes a liability when it merely restates what the next line already says. The skill is knowing which is which.
That single idea organizes everything below. The mechanism sections explain why human memory and formal syntax leave specific gaps. The quantified sections describe how often those gaps get hit. The edge cases show where comments fail or backfire. The practical sections give thresholds and templates you can apply on the next file you open. By the end you should be able to look at any comment and decide, in seconds, whether it is load-bearing or noise.
The mechanism: what code cannot say about itself
A program is a complete specification of behavior and an incomplete record of intent. The compiler checks that the syntax is well-formed and the types line up, but nothing in the language forces the author to state why a loop starts at index 1, why that timeout is 400 milliseconds, or why the obvious-looking refactor would break a downstream consumer. Those facts live in the author's head, in a ticket, in a chat thread, or nowhere. A comment is the only artifact that survives when all of those disappear.
The deeper mechanism is about the gap between what a reader can infer and what the author knew. Reading code, a competent engineer can usually reconstruct what it does within seconds. Reconstructing why takes far longer, because the "why" depends on external constraints—a vendor API quirk, a regulatory deadline, a bug in a dependency, a customer commitment. The comment exists to transfer that external knowledge across time and across people. When it is missing, the next reader has two options: trust the code blindly or reverse-engineer the constraint from scratch. Both are expensive.
There is a second mechanism, less discussed: comments are a form of executable thinking. Writing "this function must not be called before the connection pool is warmed" forces the author to check whether that is actually true. In practice, a meaningful fraction of comments are written and then deleted because the act of writing revealed that the invariant did not hold or the code did not need the explanation. The comment is a checkpoint, not just a note.
A third mechanism concerns the psychology of the reader under pressure. During an incident at 2 a.m., an on-call engineer reads code in a state of elevated stress and reduced working memory. A comment that states the operational constraint in one line—"do not reorder these two calls; the second depends on the first flushing"—prevents a change that would take the system down again. That is not documentation for its own sake. It is a load-bearing safety rail placed exactly where a tired person will need it.
Quantified context: the reading-to-writing ratio
The most cited figure in software engineering folklore is that code is read roughly ten times more often than it is written. Treat that as a directional estimate, not a measured constant; the ratio varies enormously by codebase maturity and team size. What is more useful is the structure of the cost: a comment costs the author a few seconds and saves the reader minutes, and the reader is multiplied by every future maintainer. A single well-placed sentence explaining a non-obvious constant can prevent the same investigation from being repeated by five engineers over three years.
Consider a concrete accounting. Suppose a reader spends 40 minutes reverse-engineering why a retry loop uses a 30-second ceiling instead of the default. If the comment costs the author 30 seconds and prevents that investigation four times over the life of the code, the net saving is roughly two and a half hours of engineering time. Multiply across the dozens of such decisions in a mid-sized service and the numbers compound. This is why the return on comments is highest precisely where the code is least self-explanatory—and lowest where the code already says everything.
The ratio also explains a counterintuitive pattern: over-commented code is often worse than under-commented code, because every redundant comment is a maintenance obligation that can drift out of sync with reality. A comment that says "increment i" next to i++ will eventually be wrong after a refactor and will then actively mislead. Noise comments cost attention without returning information, and they train readers to skip comments entirely—including the ones that matter. The goal is not volume. It is the highest information density per line of comment.
What information belongs in a comment
The most useful comments answer questions the code cannot. In practice, that means intent, constraints, units, failure modes, external references, and the reasons a simpler approach was rejected. A comment that explains what the code does is usually redundant; one that explains why it does it that way is usually valuable.
Intent comes first. State what the code is supposed to accomplish, especially when the implementation is indirect. A binary search that returns the insertion point rather than a boolean needs a comment stating that contract, because the signature does not make it obvious. Constraints come second: preconditions, postconditions, thread-safety assumptions, and the ranges of valid inputs. Units come third, and they are the most quietly dangerous omission. A parameter named timeout tells you nothing about whether it is seconds, milliseconds, or ticks; a parameter named timeoutMs tells you, but a comment stating the allowed range and why the default was chosen tells you more.
Failure modes deserve their own line. If a function can throw, retry, return a partial result, or silently drop data, the reader needs to know. External references matter when the behavior is dictated by something outside the codebase: a protocol specification, a vendor's undocumented limit, a regulatory requirement, or a bug tracker entry. And finally, the rejected alternative is often the single most valuable comment, because it answers the question every future maintainer will ask: why not do the obvious thing?
State intent and the contract, not the mechanics of each line. Record units and valid ranges for every numeric parameter. Note preconditions, side effects, and thread-safety assumptions. Document failure modes: exceptions, retries, partial results, silent drops. Link to the external source when behavior is dictated by a spec or a bug. Explain why a simpler or more obvious approach was rejected.
Red flags: comments that hurt
Not every comment is neutral. Some actively harm readability and trust. The following patterns should be treated as warnings, not style preferences.
A comment that restates the code line for line adds reading cost and will drift out of sync after the next refactor. A commented-out block of dead code left "just in case" becomes a maintenance trap; version control already remembers it. A TODO with no owner, date, or ticket link is a promise nobody will keep and a question nobody will answer. A comment that explains what the code does while hiding a bug in the code itself can slow the reviewer who would otherwise have caught it. A unit-less numeric parameter with no range comment invites callers to pass the wrong magnitude.
The professional caveat here is worth stating plainly: comments do not substitute for clear code. If a function needs three paragraphs to explain what it does, the first fix is to rename variables, split the function, and simplify control flow. A comment should explain what remains non-obvious after the code has been made as self-explanatory as it reasonably can be. Writing comments instead of refactoring is a common failure mode, and it produces documentation of a problem rather than a solution.
A comparison: comments against the alternatives
Comments are one tool among several for conveying intent, and each alternative has a different cost and shelf life. The table below compares them across the dimensions that matter in practice: what each can express, how reliably it stays current, and where it fails.
| Method | Best for | Staleness risk | Where it breaks down |
|---|---|---|---|
| Inline comment | Non-obvious intent, units, constraints | Medium—drifts after edits | Cannot express behavior over time |
| Function docstring | Contract, parameters, return values | Low to medium | Rarely updated when behavior changes subtly |
| Descriptive naming | Everyday intent for simple logic | Very low | Cannot carry ranges, history, or rationale |
| Tests as specification | Executable examples of required behavior | Low—fails loudly when wrong | Silent on why behavior was chosen |
| Commit message and history | Why a change was made at a point in time | Low | Invisible to a reader inside the file |
| External design document | Architecture and cross-cutting rationale | High—easily abandoned | Unreachable at the moment of reading code |
| Ticket or issue tracker | The original problem and its context | Medium | Requires network access and a live link |
The comparison makes one thing clear: comments win on locality. They are the only artifact that appears exactly where the reader is looking, at the moment of confusion. Commit messages and design documents often contain richer rationale, but a reader debugging a single function will not think to search the history. The strongest practice combines them—keep the short rationale inline and defer the long history to the commit or the ticket.
A timeline: how a comment's value changes as code ages
Comment value is not static. It peaks early, decays if unmaintained, and can turn negative when it becomes stale. The table below maps a typical lifecycle, using the qualitative stages that matter more than precise dates.
| Stage | Age (typical) | What the comment contributes | Main risk |
|---|---|---|---|
| Authoring | Day 0 | Captures intent and constraints while fresh | Restating the obvious instead of the non-obvious |
| Review | Days 1 to 14 | Helps reviewers verify the contract | Reviewer misses a mismatch between comment and code |
| Active development | Weeks to months | Guides refactors and prevents regressions | Drifts silently as surrounding code changes |
| Maintenance | Months to years | Explains inherited decisions to new owners | Becomes outdated and misleads if not updated |
| Legacy | Years | The only surviving record of original intent | Author has left; nobody can verify accuracy |
| Refactor or migration | Any | Flags hidden dependencies and invariants | Old comment carried forward into new code unchanged |
The practical reading of this table is that comments need maintenance, and maintenance needs a trigger. The natural trigger is any change to the code the comment describes. A healthy review process treats a stale comment as a defect, not a cosmetic issue. The professional caveat: no team I know of maintains comments perfectly, so treat comments as high-value evidence rather than ground truth, and verify any load-bearing claim against the code or the tests.
Practical actions: what to write, and how much
Start with code you are changing right now. Before you commit, ask three questions of each non-trivial block: would a new reader have to guess why this is here, what units these numbers use, or what would break if this changed? If the answer is yes to any of them, a comment belongs there. If the answer is no to all three, skip it—silence is better than noise.
Use a short template. One line for intent, one for the non-obvious constraint, one for the consequence of violating it. For a function, state the contract in the docstring: what it accepts, what it returns, what it throws, and what it assumes. For a number, name the unit in the identifier and state the valid range in a comment. For a workaround, link the ticket or specification and state the version of the dependency it applies to, so a future reader knows when it becomes obsolete.
Set a threshold for when a TODO is acceptable. A TODO should carry an owner or team, a linked ticket, and enough context that a stranger could act on it. A TODO without a ticket is a comment that will never be resolved; either file the ticket or delete the comment. Treat any TODO older than a few quarters as a signal to re-evaluate, not a reason to leave it indefinitely.
Review comments the way you review code. If a pull request changes behavior and the nearby comment still describes the old behavior, request a change. This is the single highest-leverage habit: it keeps the comment set correlated with the code, which is what makes comments trustworthy at all. Without that habit, the best-written comments in the repository become unreliable within a year.
Finally, calibrate against the reader. Write for the engineer who joins the team in six months and has none of your context. That reader is the honest test of whether a comment earns its place. If the sentence would help that person, keep it. If it would only help someone who already knows the answer, delete it.
Limitations: what comments cannot do, and where to rely on tests
Comments are claims, and claims can be false. Nothing in a compiler verifies that a comment matches the code, which is why comments and code diverge so reliably under pressure. Tests, by contrast, fail loudly when behavior changes. The correct division of labor is to let tests specify and enforce behavior, and let comments carry the rationale and constraints that tests cannot express. When a decision is critical, express it in both places: a test that would fail if the invariant broke, and a comment that explains why the invariant exists.
Comments also do not survive translation well. Code is copied between repositories, and the comment travels with it even when the surrounding context—the ticket link, the team convention, the dependency version—does not. A comment that was precise in its original home becomes misleading in a new one. Defensive practice is to keep each comment self-contained enough to be meaningful outside its original file, and to avoid referencing internal shorthand that a new reader cannot resolve.
The final limit is human. Most comments are written once and read many times under time pressure, and no process fully prevents drift. Treat the comment set as an imperfect record, prioritize the ones that guard safety-critical behavior, and accept that a small number of stale comments will always exist. The goal is not perfection; it is a comment set whose information density is high enough that readers can trust it where it matters.
This article is general educational information about software engineering practice, not professional advice for any specific codebase, system, or organization. It does not promise any outcome, including reduced defect rates or maintenance cost. For decisions affecting production safety, security, or regulatory compliance, consult a qualified engineer or the relevant authoritative standard.