Short-Lived Branches: Why Integration Frequency Matters
A short-lived branch is one that is created, developed and integrated before it has time to diverge meaningfully from the branch it will merge into. In practice that usually means hours, sometimes a day or two.
This lesson is about one variable, because it turns out to be the variable that predicts most of a team’s Git pain: how long work stays unintegrated.
The mechanism
Section titled “The mechanism”The moment you branch, two histories begin evolving independently. Every commit that lands on main
and is not on your branch is a difference somebody will eventually reconcile.
A trunk lane labelled main with six commits A through F. A branch lane labelled feature leaves main after commit A and has three commits of its own, X, Y and Z, never merging. Both lanes have moved on independently since the shared commit A.
The cost of reconciling is not proportional to the number of changes. It is driven by how those changes interact:
- Changes to unrelated files cost nothing — Git combines them without asking.
- Changes to different parts of the same file usually cost nothing.
- Changes to the same lines produce a conflict a human must resolve.
- Changes that are individually fine but semantically incompatible produce something worse: a clean merge that does not work.
As each side grows, the number of possible interactions grows faster than the number of changes. That is why doubling a branch’s lifetime tends to more than double the integration effort.
The silent failure
Section titled “The silent failure”Textual conflicts are the visible cost, and they are not the expensive one. Git tells you about them.
The expensive case is the semantic conflict: two changes that touch different lines, merge cleanly, and are wrong together.
You rename calculate_total() to calculate_order_total() and update all four call sites. Meanwhile a
colleague adds a fifth call site on their branch, using the old name. Both branches are correct in
isolation. Git merges them without complaint, because you edited different regions. The result does not
compile.
That example is benign because a compiler catches it. Replace it with a change to a validation rule and a new code path that assumes the old rule, and nothing catches it until production.
Short-lived branches reduce this class of problem for a simple reason: the fewer changes in flight simultaneously, the fewer opportunities for two of them to be quietly incompatible.
What short-lived actually means
Section titled “What short-lived actually means”Rather than a number, use these tests. A branch is short-lived enough if:
- It can be reviewed in one sitting without the reviewer losing context.
- You can hold its full contents in your head while working on it.
- Rebasing or syncing it is uneventful rather than a task you schedule.
- Nobody says “let me finish this first” when asked to integrate.
A branch fails those tests long before it becomes a legendary merge. The signals are noticeable early:
you start merging main in “to keep it current”, conflicts recur in the same files, the diff no longer
fits on a screen.
How teams keep branches short
Section titled “How teams keep branches short”Branch lifetime is not something you can decide directly. It is an outcome of four other things.
1. Smaller units of work
Section titled “1. Smaller units of work”The dominant factor. A branch cannot be short if the work on it is large.
Decomposing means finding intermediate states that are safe to integrate even though the feature is incomplete:
- Add the new database column in one branch; use it in another.
- Add a new function nothing calls; wire it up next.
- Introduce an interface, add a second implementation behind it, switch, remove the old one — four branches, each independently safe.
That last pattern is branch by abstraction, and it is how large refactors ship without long-lived branches.
2. Faster review
Section titled “2. Faster review”Branch lifetime has a hard floor at review latency. If review takes a day, branches live at least a day.
What helps: smaller pull requests (a fifty-line change gets reviewed far sooner than a thousand-line one), treating review as a scheduled interrupt rather than background work, and tiering by risk so low-risk changes need less ceremony.
3. Feature flags
Section titled “3. Feature flags”When work genuinely cannot be finished quickly, integrate it disabled. The code reaches main daily;
the behaviour is released later. Trunk-Based
Development covers the techniques and their
costs.
4. Trustworthy CI
Section titled “4. Trustworthy CI”Slow or flaky CI extends branch lifetime directly — people batch changes to avoid waiting, and batching is exactly what makes branches long.
Practical policies teams adopt
Section titled “Practical policies teams adopt”These are conventions, not Git features. Their value is in being explicit rather than assumed:
| Policy | Effect |
|---|---|
| Target pull requests reviewable in 15 minutes | Bounds change size |
| Review within one working day | Bounds the lifetime floor |
| Delete branches automatically on merge | Keeps the branch list meaningful |
| Flag branches inactive for N days for review | Surfaces abandoned work |
| Require branches up to date before merging | Prevents merging against a stale base |
| Prefer several small pull requests to one large one | Makes decomposition the default |
Auditing what you have
Section titled “Auditing what you have”Before changing anything, look at the current state:
git for-each-ref --sort=-committerdate refs/heads/ \ --format='%(committerdate:relative)%09%(refname:short)'3 minutes ago main2 days ago feature/search-ranking5 weeks ago spike/new-parserFor remote branches, substitute refs/remotes/origin/.
Then check how far each has diverged:
git rev-list --left-right --count main...feature/search-ranking14 3Fourteen commits on main that the branch has not seen; three on the branch. The first number is the
one that predicts pain.
To find branches nobody is working on any more:
git branch --merged mainEverything listed is already integrated and safe to delete.
Rescuing a branch that is already too long
Section titled “Rescuing a branch that is already too long”The advice above prevents the problem. It does not help with the branch you already have. Three approaches, in order of preference.
Split it into landable pieces. The best outcome, and more often possible than it looks. Identify the parts that are independently safe — a new module nothing calls, a test-only change, a refactor with no behaviour change — and land those first as separate branches. The remainder shrinks, sometimes dramatically.
git switch -c extract/parser-helpers maingit checkout feature/big-rewrite -- src/parser/helpers.pygit commit -m "Extract parser helpers"git checkout <branch> -- <path> copies specific files from another branch into your working tree and
index without switching to it. That gives you a small branch carrying one coherent slice.
Integrate it behind a flag. If the work is coherent but incomplete, merge it disabled rather than letting it age further. The divergence stops immediately; finishing continues on the trunk.
Bring it up to date, then finish it quickly. If neither of the above applies, at minimum stop the divergence growing:
git switch feature/big-rewritegit fetch origingit merge origin/main # or rebase, if the branch is privateThen treat finishing it as the priority rather than one task among several. A branch that is already expensive gets more expensive every day it stays open.
Anti-patterns
Section titled “Anti-patterns”The “spike” branch that becomes production code. An exploratory branch is fine. One that quietly becomes the real implementation after three weeks is a long-lived feature branch with a misleading name.
The personal long-running branch. dev/alice that never merges is a private fork. All the divergence
cost, none of the review.
Repeatedly merging main in to “stay current”. Reasonable occasionally. Done daily out of habit it
clutters the branch with merge commits, makes review harder, and — importantly — does not reduce the
divergence that matters, because your changes still have not reached anyone else.
Batching several finished changes into one pull request because opening three feels like more work. It is less work for you and considerably more for the reviewer.
Blocking integration on unrelated work. “I will merge once the tests I am also writing are done” converts a one-hour branch into a three-day one.
Treating branch age as the target. Age is a symptom. A team that closes branches quickly by splitting work arbitrarily, so that no individual branch does anything coherent, has optimised the metric and lost the point.
When a longer branch is the right call
Section titled “When a longer branch is the right call”Not every long branch is a mistake:
- A genuine spike. Exploring an approach you may discard. It should be deleted or decomposed, not merged.
- A change that is not safely decomposable. Some migrations only make sense atomically. Take the cost deliberately, sync often, and integrate as soon as it is coherent.
- An external contribution. You do not control a contributor’s pace.
- A release branch. Deliberately long-lived, with a defined purpose and only fixes applied. See Release Branches.
The distinction is intent. A branch that is long because someone decided it must be is different from one that is long because nobody decided anything.
Mental Model
Section titled “Mental Model”A branch is a loan against future integration effort.
Opening one borrows time now — you can work without coordinating. Every day it stays open accrues interest, and the interest compounds. Merging repays the loan.
Short-lived branches keep the loan small enough that repayment is trivial. Long-lived ones eventually cost more to repay than the work was worth.
What You Learned
Section titled “What You Learned”- Branch lifetime predicts integration cost better than change count does.
- Integration cost grows with how changes interact, so it rises faster than linearly with divergence.
- Semantic conflicts — clean merges that are wrong — are the expensive failure, and only tests catch them.
- Branch lifetime is an outcome of change size, review latency, feature flags and CI speed.
- Merging
maininto a branch repeatedly does not address the divergence that matters. - Deliberately long-lived branches exist; the problem is branches that are long by accident.
Try It Yourself
Section titled “Try It Yourself”- In any repository you work in, run the branch-age command above. How old is the oldest branch?
- Pick the oldest and run
git rev-list --left-right --count main...<branch>. - Run
git diff --stat main...<branch>— note the three dots — to see its true size. - Estimate how long integrating it would take, then ask whoever owns it.
- Run
git branch --merged mainand delete anything listed that is no longer needed.
Step 4 is usually instructive. Estimates for old branches are consistently optimistic, which is itself the argument for not creating them.
Next Lesson
Section titled “Next Lesson”Release branches are the main case where a deliberately longer-lived branch earns its cost.