A long-lived branch is a merge you keep postponing, and the cost is not linear. It is exponential.



Somewhere in your repository is a feature branch that was created two weeks ago, sits dozens of commits behind main, and that nobody wants to review. That branch is not progress. It is a merge you have been deferring, and it gets more expensive every day it stays open.

The cost is not linear. As one analysis puts it, a two-day branch is roughly a normal merge, while a two-week branch is a project, because by the time it returns to main, hundreds of unrelated commits have landed, functions have been renamed, and Git resolves the textual conflicts while nobody resolves the semantic ones (Mergify, 2026). Trunk-based development exists to stop that from ever building up, and DORA's research backs it as a high-performance practice, defining a short-lived branch as one that lives under 24 hours (Mergify, 2026).

Trunk-based development is simple to state: everyone integrates small changes into main frequently, branches live hours rather than weeks, and unfinished work hides behind feature flags instead of in a branch. This post is the practical version, the workflow and the handful of pieces that make it hold up, with the configuration to put them in place. The model is easy. The discipline is the work.

The whole model fits in one loop

The workflow itself is unremarkable, which is the point. Pull main, branch off for a few hours, make a small change, open a pull request, let CI and a reviewer check it, merge, delete the branch. Tomorrow morning, pull main and do it again.

# Start from the latest main, every time
git checkout main
git pull

# Branch for a few hours of work, not a few weeks
git switch -c fix/null-check-on-user-lookup

# small change, then
git commit -am "Add null check to user lookup"
git push -u origin HEAD

gh pr create --fill
# review + CI pass, then merge and delete the branch
gh pr merge --squash --delete-branch

That is the entire model. A real project running this way shows what the rhythm looks like in practice: 145 pull requests over three months, roughly 1.6 a day, with most branches opened and merged the same day (DEV, 2026). There is never a merge week, because integration is just what mornings look like.

The trade-off is that this only works with the practices below in place. Skip them and trunk-based development quietly degrades back into feature branching with a different name. It is also getting more important, not less: when an AI agent writes code against a branch while main moves on underneath it, a long-lived branch means half the agent's assumptions are wrong by the time you merge (DEV, 2026).

By the time a two-week branch comes back to main, hundreds of unrelated commits have landed. Git resolves the textual conflicts. Nobody resolves the semantic ones. Main breaks, the author spends a day debugging code they did not write, and the team coins the term merge week (Mergify, 2026).

Keep main releasable: gate every merge with CI

Trunk-based development assumes main is always stable and ready to deploy, which only holds if every change is tested before it lands and main itself is tested after. This is not optional. If build and test are automated but developers work on isolated, infrequently integrated branches, continuous integration is not actually happening (Atlassian).

# .github/workflows/ci.yml
on:
  pull_request:          # gate every PR before it can merge
  push:
    branches: [main]     # and verify main after each merge

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Running on both the pull request and the push to main means nothing merges without passing, and you find out immediately if a merge somehow broke the trunk. Make this a required status check in branch protection, so a red build genuinely blocks the merge rather than just being a suggestion.

The trade-off is that this CI has to be fast and trustworthy, or the short-branch rhythm stalls. A gate that takes thirty minutes turns a same-day branch into a two-day one, and a suite full of flaky tests trains people to merge past red. Speed and reliability of the pipeline are prerequisites for this model, not luxuries.

Stop branches from drifting: fail the build when they fall behind

Painful merges come from drift. The longer a branch goes without pulling main, the further it diverges, and the conflict count climbs fast: roughly zero on day one, a couple by day three, a dozen by day seven, and by day fourteen people start talking about rewriting the change from scratch (DEV, 2026). A cheap guard in CI is to fail a branch that has fallen too far behind, forcing it to integrate before it gets out of hand.

# Fail the PR if it has drifted too far from main
behind=$(git rev-list --count HEAD..origin/main)
echo "This branch is $behind commits behind main."
if [ "$behind" -gt 50 ]; then
  echo "Too far behind. Pull main and re-integrate before merging."
  exit 1
fi

The check counts how many commits have landed on main that the branch has not yet absorbed, and stops the merge once that number crosses a threshold. The fix is the discipline the model wants anyway: pull main into your branch frequently so you are always working against current code (Trunk Based Development).

The trade-off is choosing the threshold. Too tight and you create needless churn on a busy repository, too loose and it never fires. It is a blunt backstop, not a strategy, and the merge queue further down handles the same problem more precisely at scale.

Ship incomplete work behind a flag, not in a branch

The obvious objection is that some features take longer than a day. The answer is not a long branch, it is a feature flag. You merge the work into main with the flag turned off, so the code is integrated and deployed but dormant, and you flip it on later to release, with no redeploy. This is branch by abstraction: even a large feature is built in small daily merges, none of which changes user behaviour until you choose (Harness, 2026).

# Merged to main with the flag OFF. Flip it to release, no redeploy needed.
if feature_flags.enabled("new_pricing_engine", user):
    price = new_pricing_engine.quote(cart)
else:
    price = legacy_pricing.quote(cart)

The new code path lands in main immediately and runs for nobody until the flag is enabled, at which point you can switch it on for 1 per cent of users and widen gradually. This is one of the five things that make trunk-based development work in practice, alongside branches under two days, pull requests under a couple of hundred lines, automated tests gating every merge, and deploying on merge to main (DEV, 2026).

The trade-off is that flags are debt. Every flag is a branch in your logic that has to be tested and, once the feature is fully rolled out, removed. A codebase full of stale flags is its own kind of mess, so flags need an owner and a removal date, the same as anything else you do not want to accumulate.

One team moved from Gitflow, where pull requests averaged around 2,000 lines and took months to sort out, to trunk-based development with feature flags, and now deploys to production four times a day with significantly fewer conflicts (Flagsmith, 2025). The change was how and when they integrated, not what they were building.

Serialise merges at scale with a merge queue

At a small scale, the loop above is enough. At a larger one, a subtle problem appears: two pull requests can each be green against main on their own, yet break main when both land, because neither was tested against the other. Monorepos make this worse, since the surface area for conflicts is enormous, which is exactly why the big monorepo shops run trunk-based development with extra machinery (Mergify, 2026). A merge queue solves it by testing each change against the latest main, in order, just before it lands.

# Run the same checks for PRs and for the merge queue
on:
  pull_request:
  merge_group:           # GitHub tests each queued PR against current main

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

With the queue enabled in branch settings, a merged pull request does not go straight into main. It joins a queue where it is rebased onto the current tip and retested, so what lands has been verified against the exact state it is landing into. Main stays green even when many people merge at once.

The trade-off is a little latency between approval and landing, since changes wait their turn through the queue. On a busy repository that delay is a small price for a main branch that never breaks from a race between two good changes. On a small team merging a few times a day, you probably do not need it yet.

The part worth sitting with

So go and look at the oldest open branch in your repository, and be honest about what it is. It is not a feature in progress. It is a merge nobody has had to pay for yet, sitting there quietly compounding, and the longer it waits the more it will cost when it finally comes due. Trunk-based development is not a clever technique, it is a refusal to let that debt build up: small branches that live for hours, a CI gate that keeps main deployable, flags that let you merge unfinished work safely, and a queue that keeps the trunk green under load. None of those pieces is hard. What is hard is giving up the comfortable illusion that an isolated branch is free, when the bill is just being deferred and quietly growing interest. Merge little, merge often, and the terrifying merge simply stops existing.

Author note

I am Manjunaathaa, an Associate DevOps Engineer at Frigga Cloud Labs. I work across AWS, GCP, and Azure daily, with GitHub Actions as my deployment backbone. My focus is Proactive Resilience: a short-lived branch is the tightest feedback loop you can give a team, because the sooner code meets main, the sooner you learn whether it actually works together. Every practice in this post is something I actually run in production, not something I read about. I wrote this because long-lived branches are the most expensive habit teams refuse to count as a cost. The thing I keep running into is teams that automated their pipeline but still let work sit unmerged for a week, then blame the merge instead of the wait. I keep branches measured in hours, ship behind flags rather than holding code hostage in a branch, and let CI on main be the thing that tells me the truth. Merge little, merge often, and the scary merge stops existing. Let's connect on LinkedIn → Manjunaathaa.

Post a Comment

Previous Post Next Post