All posts
Engineering
Product

Shipping Fewer Bugs Without Shipping Slower

For teams deploying several times a day, most escaped defects don't come from moving fast — they come from checks that quietly became optional. Five practices that hold up under deadline pressure.

9 min readPatchlight Team · Engineering

Every team that ships daily has had the same retro. A bug reached production, everyone agrees it was catchable, and the honest root cause is that the check which would have caught it was skipped — not out of negligence, but because it was optional and the week was full. The fix that gets written down is "be more careful." It never survives contact with the next release.

The speed-versus-quality tradeoff is mostly false at this scale. Teams that deploy ten times a day are not shipping more bugs because of the frequency; small changes are easier to review, easier to revert, and easier to attribute when something breaks. What actually correlates with escaped defects is how much of the safety net depends on someone remembering to use it.

It helps to lay the whole net out first. A change passes through a fixed sequence of gates on its way to production, and each one catches a class of defect that no earlier gate could have seen:

Every gate catches a class the previous one structurally can'tCost is the honest ordering — the later a defect is caught, the more of it is rework rather than typing. Marked gates run without anyone remembering to start them.
  1. TicketMisunderstandings, before a line is writtenminutes
  2. TestsThe regressions you thought to predictminutes
  3. Automated reviewautoMechanical defects on every diff — null paths, unhandled rejections, missing ownership checksminutes
  4. Human reviewThe wrong approach, taken competentlyan hour
  5. Merge-time task checkautoCode that works and isn't what the ticket asked forhours
  6. Scheduled scanautoEverything the diff never showed anyonea day
  7. ProductionYour users find itdays, plus trust

1. Make the first review pass unconditional

Human review is the highest-value check a team has and the first one to degrade under load. It doesn't disappear — it becomes an approval. Somebody skims the title, sees a familiar author, and clicks. The review happened on paper and caught nothing.

The way out is not more discipline, it's changing what the human is asked to do. If an automated reviewer has already read every line by the time a person opens the PR — flagged the null dereference, the unhandled promise rejection, the missing ownership check — then the human review starts from a different place. Instead of scanning for mechanical defects, the reviewer is looking at whether the approach is right, which is the part no tool does well and the part they're uniquely qualified for.

Patchlight posts that first pass as inline comments on every pull request and commit, before anyone is assigned. It is not gated on anyone remembering to trigger it, which is the entire point.

2. Shrink the change, not the checklist

Review quality falls off a cliff somewhere around 400 lines. Past that, reviewers stop reading and start pattern-matching, and the defect density of what gets approved climbs sharply. Every team knows this and every team ships the 2,000-line PR anyway, usually near a deadline.

What a reviewer still catches, by size of the changeSchematic, not measured — the shape is the well-replicated finding from code-review research (the ~200–400 line ceiling is usually cited to Cisco/SmartBear's 2,500-review study). No Patchlight data on either axis.
03006009001200~400 lineswhere review guidance tops outLines changed in one pull requestDefects a reviewer still finds

The practical read of that curve is not "review harder on big PRs." It is that past a certain size, review stops being a control at all, and the only lever left is splitting the change:

  • Land refactors and behavior changes separately — a diff that moves code and changes it is unreviewable, because the mechanical noise hides the one line that matters.
  • Merge behind a flag rather than holding a branch open. Long-lived branches convert small mistakes into large merge conflicts, and conflict resolution is where bugs get reintroduced.
  • Split by risk, not by size. A 500-line change to a test fixture is safer than a 30-line change to a permission check, and they deserve different amounts of scrutiny.

3. Scan the code nobody touched

Pull request review covers the diff. That is the correct scope for review, and it means an entire class of problems is structurally invisible: the dependency that had no known CVE when it was added, the file whose assumptions were broken by a change three sprints ago in a different service, the debug endpoint someone added during an incident and nobody removed.

These are found by scanning the repository as it currently exists, on a schedule, independent of what shipped this week. Run it against your default branch nightly or weekly and route the findings somewhere they'll be triaged — a dashboard nobody opens is not a control. Patchlight's security monitor does this on whatever cadence you set, and reports against the same finding model as review, so a scan finding and a review finding land in the same queue with the same severities.

4. Check the change against the ticket, not just against itself

The most expensive production incidents are rarely code that was wrong. They're code that was correct and did something other than what was asked. The ticket says "soft-delete expired invites"; the implementation deletes them. Every test passes — the tests were written from the implementation. Review passes — the code does what it says. Nobody re-read the ticket, because by the time the PR is open the ticket is a tab that was closed two days ago.

This is the gap our reverse task check exists to close. When a PR merges, Patchlight matches it to a Linear issue — by branch name, by issue key in the title or body, or through Linear's own attachments — and compares the shipped diff against what the ticket actually asked for. The report goes back as a comment on the issue, where the person who wrote the requirement will see it.

What the merge-time task check posts backExample report. The vocabulary is the product's own: a verdict, discrepancies typed by kind and severity, and testing notes — posted on the Linear issue when the PR merges.
ENG-482 · Soft-delete expired invitespatchlight · task check
discrepancies_found

The PR implements expiry, but deletes rows instead of marking them, and leaves the existing backlog untouched. One unrelated change is included.

  • logic_differshigh

    The issue asks for a soft delete; the implementation removes the row.

    invites.ts:212 — `db.delete(invites)`, where the issue says “mark expired, keep for 30 days”.

  • scope_missingmedium

    Nothing expires the invites that are already past their date.

    “Expire the existing backlog” is an acceptance criterion; no migration or job in the diff.

  • scope_creeplow

    Invite email copy changed alongside the fix.

    emails/invite.tsx — not mentioned anywhere on the issue.

Testing notes

Scope: invite lifecycle + the admin list that filters on it. Risk: rows deleted by this build are not recoverable if the soft-delete lands later.

Drift is reported in both directions, and that matters: a requirement no code appears to satisfy is the obvious half, but behavior the issue never mentioned is how scope creep and accidental changes reach production.

A bug is a mismatch between what the code does and what someone expected. Testing checks the code against the developer's expectation. The reverse task check compares it against the requester's.

5. Close the loop with data you didn't have to collect

Teams that improve their defect rate are the ones that can answer a specific question: what kind of bug do we ship most? Not a general sense that "we have a lot of null errors" — an actual distribution, by category, by repository, over time. Without it, process changes are guesses and nobody can tell whether last quarter's new rule helped.

Findings data answers this for free if it's being collected anyway. Patchlight groups findings by category and severity across your repositories and sends a weekly write-up: what came up, what changed since last week, what's still open. It takes about two minutes to read, and it's the input that makes the other four practices tunable instead of aspirational.

What this looks like in practice

None of this asks a fast team to slow down, and that's the constraint that matters — any process that trades throughput for safety gets abandoned the first time a deadline is real. Automatic review, scheduled scanning, and a merge-time ticket check all run on their own. The only genuinely manual practice in the list is keeping changes small, and that one makes the work faster anyway.

The floor is the goal. Not catching everything — catching the same things every time, in the weeks when nobody has the bandwidth to be careful.