Off-by-one is the bug whose name has its own punchline. It's so common because human language is ambiguous about endpoints ("between 1 and 10" — is 10 included?), and because programming-language conventions are inconsistent (Python slices are half-open, Lua arrays are 1-indexed, SQL BETWEEN is inclusive).
The shape: a loop bound that should be < is <=, or vice versa. A pagination offset that double-counts the last row of the previous page. An index n - 1 that becomes n after a refactor and silently corrupts data. A buffer allocation of len instead of len + 1 for the null terminator.
Pattern-recognition is the best defense. Reviewers learn the shapes — pagination, buffer length, slice bounds, recursion termination — and check the boundary in each case. Property-based testing helps too: a property like "first page + remaining pages contains every row exactly once" catches an off-by-one even when example tests don't.
Review heuristic
When a comparison involves <, <=, >, >=, or -1, work the boundary case by hand: what happens with zero items? One item? n+1 items? Pagination, buffer length, and recursion are the three places where the bug is most likely.
External reference
CWE-193: Off-by-one Error — the canonical industry classification for this bug class. Useful when filing tickets, writing security policies, or arguing with a static analyzer.