Async Code Review — How to Collaborate Without Blocking Each Other
You open a PR at 4pm Kathmandu time. Your reviewer is asleep in Lisbon. They review at 9am theirs, leave four questions, and go into meetings. You answer at 6pm yours. They respond next morning. A twenty-line change takes three days, and nobody did anything wrong. Async review isn’t slow because people are slow — it’s slow because most teams run a synchronous process with the synchronous parts removed.
The Real Cost Is Round Trips
The metric that matters isn’t review time, it’s number of exchanges. Each one costs a full timezone cycle.
A PR that merges in one exchange — reviewer reads, approves — takes a day. Three exchanges takes three days. So every practice below is aimed at the same thing: making the first exchange sufficient.
That reframes what “a good PR” means. It’s not just correct code; it’s a change that a reviewer can evaluate completely without asking you anything.
Front-Load Everything the Reviewer Will Need
Answer the questions before they’re asked
The four questions your reviewer will leave are usually predictable: why this approach, did you consider X, how do I test this, what’s the rollout risk. Write those into the description and they never become a round trip.
Annotate your own diff
Review your PR before requesting review, and leave inline comments on the parts that need context.
app/services/invoice_totalizer.rb:47— “Rounding half-up here rather than banker’s rounding to match the existingOrder#totalbehaviour. Inconsistent with the accounting spec but changing both is a separate PR.”
That comment prevents a question, a defence, and a follow-up. It also signals you actually read your own diff, which changes how carefully people read it.
Say what kind of review you want
**Review depth:** design-level please — the implementation is mechanical,
the question is whether `Totalizer` should own currency conversion at all.
**Skip:** the rubocop autocorrect in `app/services/` (separate concern,
happy to split it out if it's noisy).
Reviewers default to line-by-line because they don’t know what else you want. Tell them.
Make the First Exchange Decisive
The reviewer’s side of the contract: never leave a review that requires a reply before anything can happen.
Label every comment with its weight
blocking: this double-charges on retry — needs an idempotency key
suggestion: `find_each` would avoid loading 40k rows, your call
nit: `items` reads better than `arr` here
question: is the 30s timeout deliberate? (not blocking either way)
praise: the error message here is genuinely excellent
An unlabelled comment forces the author to guess whether it blocks the merge. Multiply by twelve comments and you’ve created a negotiation.
Propose, don’t interrogate
✗ “Why did you do it this way?” ✓ “I’d have reached for
filter_maphere to avoid the second pass — is there a reason against it? Not blocking.”
The first requires a reply before anything moves. The second contains a proposal the author can accept, reject with one line, or ignore.
Use GitHub’s suggestion blocks for anything mechanical — the author clicks accept instead of typing:
```suggestion
orders.filter_map { |o| o.total if o.shipped? }
```
Approve with comments
The highest-leverage habit in async review. If your only remaining concerns are non-blocking, approve and say what you’d still like changed. The author addresses them and merges without waiting another cycle for you to wake up.
“Approving — the two nits are yours to take or leave. Please do add the timeout before merging.”
Teams that adopt this consistently see review cycles drop by more than half, because most reviews were only ever one round trip away from done.
Escalate Out of the Thread
Async is the default, not a religion. Some conversations are wrong for a comment thread:
- Third exchange on the same point. You’re not converging. Two people typing past each other for three days is worse than fifteen minutes on a call.
- A disagreement about architecture. That’s a design discussion that shouldn’t have started in a diff.
- Anything with emotional temperature. Text strips tone, and both parties fill the gap pessimistically.
The move: "We're circling on this — grabbing 15 minutes tomorrow at 08:00 UTC. I'll post the outcome here." The last clause matters. A decision made on a call and not written back to the PR is invisible to everyone else and to your future self.
Make Reviews Findable, Not Pushed
Don’t DM for reviews. It creates an obligation to respond immediately, which is the thing async is supposed to remove. It also hides the request from anyone else who could have picked it up.
A shared queue works better:
- A channel where PRs post automatically, with size and age
- A daily bot summary of anything older than 24 hours
- CODEOWNERS for automatic routing, so nobody has to decide who reviews what
Example:
# .github/CODEOWNERS
/app/services/billing/ @finance-team
/db/migrate/ @backend-leads
/config/ @backend-leads
*.md @docs
And an explicit expectation, written down: first review within one business day of your timezone; if you can’t, unassign yourself so someone else picks it up. Unassigning is the important half — silent non-review is the failure mode, not slow review.
Keep PRs Small Enough to Review in One Sitting
Everything above fails against a 900-line diff. A reviewer in a different timezone has one window; if the PR doesn’t fit in it, it gets skimmed or deferred, and deferred means another day.
- Under 400 lines of substantive change
- Separate mechanical from meaningful — renames and formatting in their own PRs
- Stack dependent PRs rather than batching them, so each merges as it’s approved
A stack of four 150-line PRs reviews faster in aggregate than one 600-line PR, even accounting for the overhead, because each piece can move independently.
Pro-Tip: Push follow-up commits rather than force-pushing while a review is in flight. A force-push destroys the reviewer’s ability to see what changed since they last looked — GitHub’s “changes since your last review” diff is the single thing that makes a second pass take two minutes instead of twenty. When your reviewer is asleep during your entire working day, that diff is the only way they can re-review efficiently. Squash at merge time, where the history gets cleaned up anyway and nobody loses anything. The exception is a genuine rebase onto a moved base branch — do that, but say so in a comment so the reviewer knows why their diff view reset.
Write the Decision Down
Async review produces a record, and that’s an asset most teams throw away. When a thread resolves a real question, the outcome belongs somewhere durable:
- A short note in the PR description, not just buried in a resolved thread
- An ADR for architectural decisions
- A comment in the code for a non-obvious constraint
Six months later, someone asks why the totalizer rounds half-up. The answer exists in a collapsed thread on a merged PR that nobody will ever find.
Conclusion
Async review is slow when it takes three exchanges and fast when it takes one, so optimise for the first exchange being sufficient. Authors: annotate your own diff, answer the predictable questions in the description, and say what kind of review you want. Reviewers: label every comment as blocking or not, propose instead of interrogate, and approve-with-comments whenever your remaining notes don’t block. Escalate to a call on the third exchange and write the outcome back. Keep PRs small enough to review in one sitting, and never force-push while someone is mid-review.
FAQs
Q1: What if I disagree with a blocking comment?
Say so directly with your reasoning, in one message, and propose a resolution — “I’d rather keep it as is because X; if you still disagree let’s take it to a call tomorrow.” Don’t silently change the code you think is right.
Q2: How long should I wait before nudging?
One business day in the reviewer’s timezone, then a nudge in the shared channel rather than a DM. If it’s genuinely urgent, say why — urgency without a reason erodes fast.
Q3: Should reviewers approve code they don’t fully understand?
No, but “I don’t understand this” is a legitimate blocking comment and often the most useful one in the review. If you can’t follow it, the next person maintaining it can’t either.
Q4: Does pair programming replace review?
For the code that was paired on, largely yes — two people already looked at it. Say so in the PR description so a reviewer doesn’t duplicate the work.
Q5: How do I review a large PR I can’t split?
Review in passes and say which pass you did: “First pass — structure and interfaces only, will do correctness tomorrow.” Partial review communicated clearly beats a delayed complete one.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀