Start with the contract, not the commit label

If a documented limit=100 endpoint returns 101 rows, changing it to 100 sounds like a patch—and can still affect a client that came to rely on the extra row. The version decision turns on whether 101 was outside the published contract, not on whether the commit is labeled fix.

That distinction matters because SemVer is a promise to people who depend on your software. A patch says, in effect, that consumers can take the fix without adapting to an incompatible change. A release note that calls something a “bug fix” does not make that promise true.

Use this test: Does the new behavior preserve the public contract that consumers were entitled to rely on? If yes, a patch is usually right. If no, treat the change as an API break, even if the old behavior was unintended and the new behavior is more sensible.

Then make a separate judgment about rollout risk. A compatible correction can still surprise consumers who built workarounds around the bug. SemVer classifies compatibility; it does not measure how many users will notice.

What SemVer promises users

For a released version such as 2.14.3, SemVer’s basic mapping is straightforward: a backward-compatible bug fix increments the patch number, new backward-compatible functionality increments the minor number, and incompatible public API changes increment the major number. The promise is about what dependent software can expect when it upgrades, not how difficult the code change was.

The distinction between patch and minor is easy to blur in a bug-fix discussion. If a fix merely makes an existing documented operation behave as specified, it is not a new capability; that points to a patch. If the release adds a new option or supported operation without breaking existing use, that points to a minor release, even if the feature arrived alongside a fix.

SemVer’s compatibility promise only works when a project defines its public API. That may include more than function names: an HTTP response shape, a command’s exit code, a documented default, or a configuration file format can all be part of the interface consumers use.

For the formal rules, refer to Semantic Versioning 2.0.0. The practical question in a disputed fix is which behaviors your project has made part of its public contract.

Decide what counts as the public contract

Start with what your project publishes and supports: API documentation, type signatures, CLI help, schemas, examples, and explicit compatibility guarantees. If the docs say a function returns a sorted list, returning an unsorted list is a defect against the contract. Sorting it is a correction, not a new feature.

Do not limit the review to documentation prose. A published JSON schema may promise that a field is always present; a CLI may document that invalid input exits with code 2. Those details can be part of the interface even if the project never calls them “API.” Changing one can break scripts or integrations just as surely as removing a method.

Conversely, an implementation detail is not automatically a promise because it happened once. A private helper’s iteration order, an undocumented cache duration, or a particular error message may not be supported behavior. But if your examples, tests, or compatibility policy have encouraged consumers to rely on it, investigate before declaring it private.

Write down the evidence for the contract decision. “We intended this” is weaker than a published guarantee; “we never meant it” is not enough to erase behavior users were explicitly told to expect.

Walk through the pagination fix

Suppose an API documents limit as “the maximum number of records returned” and allows a value of 100. A defect in the query returns 101. A client can reasonably assume that the extra row is an anomaly, but it might also process every returned row or use the response count in its own logic.

Under the published contract, the correction to return at most 100 is compatible: the 101st row was never promised. That points to a patch. The fact that a consumer might notice the correction is a reason to explain it and test it, not by itself proof that the API contract changed.

Now change the facts. The API documentation says a page includes the requested item and its endpoint, or the response schema explicitly promises a particular inclusive range. A “fix” that changes the end boundary to exclusive alters a documented result. That is not a contract-preserving correction; consumers may lose an item or need to change their requests.

In that second case, a major version is the SemVer choice for a stable API. Calling the new boundary more intuitive does not make it backward-compatible. If the team instead adds a new parameter or endpoint while retaining the old behavior, that additive route may fit a minor release.

Four cases, four release decisions

Use the table to separate what the project promised from what consumers may experience. “Impact” is a practical warning, not a substitute for checking the contract.

Case Contract Consumer impact Version choice
Fix an off-by-one result count Docs promise “at most 100”; implementation returns 101 A consumer may have adapted to the extra row Patch, with a clear release note
Change a documented date boundary Docs promise an inclusive end date; fix makes it exclusive Existing requests can return fewer records Major for the changed contract; alternatively preserve old behavior and add a new option in a minor release
Correct an undocumented crash No supported behavior promises that input will crash A consumer may have worked around the crash Usually patch; assess workaround exposure
Add an optional output field Existing fields and behavior remain unchanged Some strict consumers may reject unknown fields Minor only if the API’s compatibility policy permits additive fields; otherwise treat the format’s constraints as contract

The last row is a useful warning: “additive” does not guarantee compatibility for every format or consumer. A client that validates an exact response shape may break when a field appears. Your published compatibility policy should say whether consumers must tolerate unknown fields; if it does not, do not assume that every addition is safe.

Handle undocumented behavior with evidence

Undocumented behavior is the gray area. SemVer’s public-API framing does not require a project to preserve every accidental output forever, but “undocumented” is not a magic word that makes a change harmless. A widely used workaround can turn a tiny correction into a support problem, even when it does not change the written contract.

Before choosing the bump, look for evidence that the behavior was relied upon: bug reports describing it as expected, user guides that reproduce it, tests in downstream projects, or a compatibility promise that covers the behavior. These signals do not all have equal weight. A documented guarantee is stronger evidence than a single user’s workaround, but both help predict the cost of shipping.

If the behavior contradicts an explicit guarantee, correct it with a patch and explain the correction. If the project effectively promised the behavior through published examples or policy, consider preserving it or treating the change as incompatible. If the evidence is weak but the impact could be severe, a staged rollout can reduce risk without pretending the version number answers the compatibility question.

Keep the reasoning in the release decision or issue. That gives maintainers a record to apply consistently next time, instead of re-litigating whether every surprising output was “intended.”

Keep versioning separate from rollout risk

A patch can create real migration work if consumers built workarounds around the defect. A major release can be necessary even when the implementation change is only a few lines. The size of the diff is not a compatibility test, and a high-risk rollout is not automatically a reason to mislabel the SemVer impact.

This is where automated release tooling needs a human compatibility check. Tools such as semantic-release can connect commit intent to version bumps and release notes, reducing inconsistent manual decisions. But a commit labeled fix cannot tell the tool whether the fix changes a published response shape or violates a documented default.

Agree on a team rule for that boundary. For example: “A bug fix is a patch only when it restores documented behavior without removing or changing a supported behavior.” Route exceptions to a maintainer who can review the contract and downstream impact before the release is published.

That rule also prevents a common source of friction: one developer sees an obviously incorrect implementation, while a user sees a behavior that has shaped their integration for months. Both can be right. The release decision should account for the contract and communicate the practical impact.

Roll out a compatible correction carefully

When the contract says “patch” but you expect consumers to notice, use release operations to manage surprise. A staged rollout is not required by SemVer; it is a way to learn whether a contract-compatible fix exposes hidden dependencies before every consumer upgrades.

  1. Record the expected behavior. Link the contract, such as the API sentence that defines limit, and write down the before-and-after result.
  2. Test the boundary. Add a regression test for the documented maximum and a test for the last item or page boundary, not only a typical request.
  3. Publish the exact change. State what consumers will observe and why the old output was outside the supported behavior. Avoid release notes that only say “miscellaneous bug fixes.”
  4. Use available rollout controls. If the product supports canaries, staged deployment, or a feature flag, apply one where appropriate; monitor relevant errors or support reports.
  5. Escalate if evidence changes. If testing or user reports show that the old behavior was effectively guaranteed, pause and revisit the compatibility classification rather than defending the original label.

For a major-version change, make the migration path explicit before release: show the old and new request or output, identify the adjustment consumers need, and keep the old behavior available during a transition only if your product can safely support both.

Make the decision this week

For the next behavior-changing fix on your team, do not begin with “patch or minor?” Begin with a one-sentence contract comparison: “Before, the product did X; after, it does Y; the published contract says Z.” If X violates Z and Y restores it, the normal choice is a patch. If Y changes Z, it is a breaking change for a stable API, regardless of the commit’s label.

Then make and record these checks before merging:

  • Contract: Which docs, schema, CLI help, or compatibility policy defines this behavior?
  • Compatibility: Can a supported consumer keep working without changing code or configuration?
  • Evidence: Is the old behavior explicitly promised, or merely observed? Are there known workarounds?
  • Release: Does the selected patch, minor, or major bump match the contract decision, and do the notes explain the visible change?

If your project cannot answer the first question, clarify the contract while reviewing the fix. A version number is useful only when maintainers and users can interpret the promise behind it.