6 min read
On this page

PR Workflow

Why PRs Are a Bottleneck

Pull requests are where code goes to wait. A feature that took 4 hours to build can easily sit in review for 2 days. Multiply that by every PR your team opens, and code review becomes the dominant bottleneck in your delivery pipeline.

Most teams treat this as inevitable. It's not. The speed at which PRs move through review is directly controlled by how you write them, how you request review, and how your team structures the review process.

Fast PR throughput doesn't mean rubber-stamping code. It means making it easy for reviewers to do their job quickly and thoroughly. A well-structured PR gets reviewed in hours, not days. A poorly structured one gets deprioritized, partially reviewed, and eventually approved out of exhaustion rather than confidence.

Small PRs Ship Faster

This is the single most impactful thing you can do to improve your PR workflow. The data is unambiguous:

PR size        Median time to merge    Approval rate
< 100 lines   4 hours                 High confidence
100-400 lines  1 day                   Moderate confidence
400-1000 lines 3 days                  Review fatigue, corners cut
> 1000 lines   1 week+                 Effectively unreviewed

Large PRs don't get better reviews. They get worse reviews. A reviewer looking at a 1500-line diff will skim instead of reading carefully, miss bugs instead of catching them, and approve to clear their review queue rather than because they're confident the code is correct.

How to keep PRs small

Break features into vertical slices. Instead of building an entire feature in one PR (database, API, frontend, tests), ship it in slices that each deliver a thin piece of end-to-end functionality.

Bad: One PR that adds the entire user profile feature
  - 15 files, 800 lines, touches database, API, and UI
  - Reviewer needs 2 hours to understand the full change

Good: Three PRs that build the feature incrementally
  PR 1: Add user profile database schema and model (150 lines)
  PR 2: Add user profile API endpoint with tests (200 lines)
  PR 3: Add user profile UI with integration test (250 lines)
  - Each PR is reviewable in 20 minutes
  - Each can be merged and deployed independently

Ship infrastructure changes separately. If your feature needs a new utility function, database migration, or configuration change, ship those as separate PRs first. The feature PR then only contains feature logic.

Use feature flags. Feature flags let you merge incomplete features into main without exposing them to users. This removes the pressure to complete everything before merging.

# Merge partial feature behind a flag
if (featureFlags.isEnabled('user-profiles')) {
  showProfilePage();
}

When large PRs are acceptable

Rarely. But some situations genuinely require it:

  • Automated refactors (rename across 50 files — the diff is large but mechanical)
  • Generated code (proto files, schema changes with generated types)
  • Dependency updates with lock file changes

Even in these cases, split what you can. Put the manual changes in one PR and the automated/generated changes in another.

Self-Review Before Requesting Review

Before you ask anyone to review your PR, review it yourself. Open the diff in GitHub/GitLab and read it as if you're seeing the code for the first time.

The self-review checklist

1. Read every line of the diff. Does the change make sense in context?
2. Check for debugging artifacts: console.log, TODO comments, commented-out code
3. Verify tests exist and cover the important paths
4. Check for obvious issues: typos, wrong variable names, missing error handling
5. Ensure the PR description explains what and why
6. Confirm the PR is as small as it can be

Self-review catches 30-50% of the issues that a reviewer would flag. This means fewer review round-trips, faster approval, and less of your reviewer's time wasted on trivial issues.

Leave comments for your reviewer

When you know a section of code will raise questions, preemptively explain it:

# On a complex line:
"Using a recursive approach here because the tree depth is bounded to
 5 levels max. Iterative would be cleaner but the recursion is
 easier to reason about for this case."

# On an intentional deviation from convention:
"Normally we'd use the ORM here, but this query needs a window function
 that the ORM doesn't support. Raw SQL is intentional."

This saves the reviewer from writing a "why did you do this?" comment and waiting for your response. One round-trip eliminated.

Writing PR Descriptions That Help Reviewers

A PR with no description says: "Figure it out from the diff." A PR with a good description says: "Here's what I changed, why, and what to pay attention to."

Structure of an effective description

## What

One paragraph summarizing the change. What does this PR do?

## Why

Why is this change needed? Link to the ticket/issue. Explain the
problem this solves.

## How

Brief explanation of the approach. Not a line-by-line walkthrough,
but enough context that the reviewer understands the strategy before
reading the code.

## Testing

How was this tested? Automated tests, manual testing steps, or both.
If there are edge cases that are hard to test automatically, describe
how you verified them.

## Notes for reviewer

Anything the reviewer should pay special attention to. Areas where
you're less confident, alternative approaches you considered, or
specific files to start reviewing from.

Example description

## What

Add rate limiting to the public API endpoints using a token bucket
algorithm.

## Why

We're seeing abuse from scrapers hitting the /products endpoint at
~500 req/s, which degrades performance for legitimate users.
Ticket: INFRA-2341

## How

- Added a RateLimiter middleware using a per-IP token bucket
- Default limit: 100 requests per minute, configurable per endpoint
- Rate limit headers (X-RateLimit-Remaining, X-RateLimit-Reset) are
  included in all responses
- When limit is exceeded, returns 429 with a Retry-After header

## Testing

- Unit tests for the token bucket implementation
- Integration tests verifying rate limit headers and 429 responses
- Load tested locally with wrk: confirmed rate limiting kicks in
  at the configured threshold

## Notes for reviewer

- The token bucket implementation in rate_limiter.ts is the core
  logic to review carefully
- I chose in-memory storage for now (Redis-backed is tracked in
  INFRA-2342 for when we scale to multiple instances)
- The per-endpoint configuration in rate_limits.yaml is where new
  endpoints get their limits set

A reviewer reading this knows exactly what to expect in the diff and where to focus their attention.

Stacked PRs

Stacked PRs (or "PR chains") let you continue building on top of a PR that's still in review. Instead of waiting for PR 1 to merge before starting PR 2, you branch PR 2 off of PR 1's branch.

main
  \
   PR 1: Add user model
     \
      PR 2: Add user API (depends on PR 1)
        \
         PR 3: Add user UI (depends on PR 2)

Managing stacked PRs

# Create the stack
git checkout -b feat/user-model    # PR 1
# ... make changes, push, open PR 1 ...

git checkout -b feat/user-api      # PR 2 (branched from PR 1)
# ... make changes, push, open PR 2 ...
# Set PR 2's base branch to feat/user-model (not main)

git checkout -b feat/user-ui       # PR 3 (branched from PR 2)
# ... same pattern ...

When PR 1 is updated

If PR 1 gets changes from review feedback, you need to update the stack:

git checkout feat/user-api
git rebase feat/user-model

git checkout feat/user-ui
git rebase feat/user-api

When PR 1 merges

After PR 1 merges into main:

git checkout feat/user-api
git rebase main
# Update PR 2's base branch to main (in GitHub/GitLab)

git checkout feat/user-ui
git rebase feat/user-api

Tools for stacked PRs

Manual stack management is tedious. Tools like gh (GitHub CLI), Graphite, or git-town automate the rebasing and base-branch updates:

# With Graphite
gt stack submit    # Push and create/update PRs for the entire stack

# With GitHub CLI
gh pr create --base feat/user-model --head feat/user-api

Stacked PRs are worth the tooling investment if you frequently build features that depend on code that's still in review. They eliminate the "waiting for review" bottleneck by letting you keep working.

Draft PRs

Draft PRs signal "this is not ready for full review, but I want feedback on the direction."

When to use drafts

- Early in a feature, when you want to validate the approach
  before investing more time
- When you have a working implementation but haven't written
  tests or documentation yet
- When you want to share progress with the team without
  triggering the review process
- When the PR is a proof of concept or experiment

When you open a draft, say what kind of feedback you want. "I'd appreciate feedback on the API surface design and whether in-memory storage is acceptable here." Convert to "ready for review" only when you've addressed the draft feedback.

Being a Good Reviewer

PR workflow is a two-player game. Aim to respond to review requests within 4 hours. Focus on high-value feedback: logic errors, missing edge cases, security issues, architecture problems. Leave style nitpicks to linters.

Be specific and actionable. "This could be better" is useless. "This function handles both validation and persistence — consider splitting them so each is independently testable" is useful.

Distinguish blocking from non-blocking feedback. Prefix blocking issues clearly. Mark suggestions as "Nit" so the author knows what must be fixed versus what's optional.

Common Pitfalls

Treating PR size as someone else's problem. You control how big your PRs are. If reviewers are slow, the first thing to check is whether your PRs are too large.

Skipping the PR description. "The code speaks for itself" is almost never true. Even simple PRs benefit from a one-line description of why the change was made.

Opening PRs before self-reviewing. Every typo and debugging artifact you ship to review is a round-trip that adds hours to the review cycle.

Letting PRs go stale. If a PR hasn't been reviewed in 2 days, follow up. If it hasn't been reviewed in a week, something is structurally wrong with your team's review process.

Blocking on review when you could stack. If you're waiting for PR 1 to merge before starting PR 2, and the two are related, use stacked PRs. Don't let the review queue determine your development pace.

Key Takeaways

Small PRs get faster, better reviews. Keep PRs under 400 lines. Break large features into vertical slices or use feature flags to ship incrementally.

Self-review catches the easy issues before your reviewer sees them. Always read your own diff before requesting review.

PR descriptions are not optional. Explain what changed, why, how it was tested, and where to focus the review.

Stacked PRs let you keep working while earlier PRs are in review. The tooling investment pays for itself quickly.

Fast review turnaround is a team responsibility. Review within 4 hours, focus on high-value feedback, and clearly distinguish blocking issues from suggestions.