Git Flow Explained: Workflow, Branches, Pros and Cons
Git Flow is a branching model with two long-lived branches — main and develop — plus three
short-lived branch types: feature, release and hotfix. It was published by Vincent Driessen in 2010 in
a post titled A successful Git branching model, and for several years it was the default answer to
“how should we use Git?”
It is now widely criticised, sometimes unfairly. The model solves real problems; the criticism is mostly that many teams adopted it without having those problems.
Historical context
Section titled “Historical context”In 2010, most software was released, not deployed. You cut version 1.4, tested it for a fortnight, shipped it to customers, and then supported it while 1.5 was being built. Some customers stayed on 1.3 and still needed security fixes.
That world has specific requirements. You need somewhere to stabilise 1.4 that does not block work on 1.5. You need to fix 1.3 without shipping unrelated new features. You need a record of exactly what each version contained.
Git Flow answers all three, and its complexity is the shape of those answers. Driessen later added a note to the original post observing that teams building continuously delivered web software are probably better served by a simpler model — an unusually honest revision by an author whose work had become an industry default.
The branches
Section titled “The branches”Four lanes. The top lane, main, has an initial commit and two release tags. Below it, develop runs continuously and receives feature work. A feature lane branches from develop and merges back. A release lane branches from develop, receives stabilisation commits, and merges into both main and develop.
| Branch | Lifetime | Branches from | Merges into | Purpose |
|---|---|---|---|---|
main | Permanent | — | — | Production-released code only; every commit is a release |
develop | Permanent | main (once) | — | Integration branch for the next release |
feature/* | Temporary | develop | develop | One feature |
release/* | Temporary | develop | main and develop | Stabilise a version |
hotfix/* | Temporary | main | main and develop | Urgent production fix |
The two rules that carry most of the model:
main contains only released code. Every commit on main is a released version, normally tagged.
You can always answer “what is in production?” by reading main.
Release and hotfix branches merge into two places. This is the detail people forget and the source
of the model’s most common bug. A fix made on a release branch must reach develop too, or the next
release silently reintroduces the problem.
How each branch type works
Section titled “How each branch type works”Feature branches
Section titled “Feature branches”Ordinary feature branches, with one difference: they start from and return to develop, not main.
git switch developgit pullgit switch -c feature/user-search# … work …git switch developgit merge --no-ff feature/user-searchgit branch -d feature/user-search--no-ff is conventional in Git Flow. It forces a merge commit even when a fast-forward is possible, so
the branch remains visible as a unit in the history. Fast-Forward vs Three-Way
Merge explains what that flag actually does.
Release branches
Section titled “Release branches”When develop contains enough for a release, it is branched.
git switch developgit switch -c release/1.1From this point:
developis free again. Work on the following release continues immediately.- The release branch accepts only stabilisation. Bug fixes, version bumps, changelog entries — no new features.
- When it is ready, it merges into
mainand is tagged.
git switch maingit merge --no-ff release/1.1git tag -a v1.1.0 -m "Release 1.1.0"git switch developgit merge --no-ff release/1.1git branch -d release/1.1Hotfix branches
Section titled “Hotfix branches”A production defect needs fixing without shipping whatever develop currently contains. Hotfix
branches start from main:
git switch maingit switch -c hotfix/1.1.1# … fix …git switch maingit merge --no-ff hotfix/1.1.1git tag -a v1.1.1 -m "Hotfix 1.1.1"git switch developgit merge --no-ff hotfix/1.1.1git branch -d hotfix/1.1.1The same two-target rule applies. If a release branch is currently open, the hotfix normally merges into
that instead of develop, so it is not lost when the release lands.
Support branches
Section titled “Support branches”The original model mentions long-lived support branches for maintaining older versions — a support/1.x
branch that receives backported fixes long after 2.0 ships. They are optional and only relevant if you
genuinely support multiple released versions concurrently. If you do, this is one of the strongest
arguments for a Git Flow-shaped model.
A release cycle end to end
Section titled “A release cycle end to end”A concrete pass through the model, for a team shipping version 1.1 of an installed product.
-
Feature work accumulates on
develop. Three developers landfeature/user-search,feature/export-csvandfix/timezone-parsingover two weeks. Each branched fromdevelopand merged back with--no-ff. -
Scope is frozen. The team decides 1.1 is those three changes.
Terminal window git switch develop && git pullgit switch -c release/1.1git push -u origin release/1.1developis immediately available again — work on 1.2 starts the same afternoon. -
Stabilisation. QA tests the release branch. Two defects are found and fixed on the release branch, not on
develop:Terminal window git switch release/1.1git commit -am "Fix CSV export encoding on Windows"A release candidate can be tagged from here if the process calls for one:
Terminal window git tag -a v1.1.0-rc.1 -m "Release candidate 1" -
Ship. The release branch merges into
mainand is tagged:Terminal window git switch main && git pullgit merge --no-ff release/1.1git tag -a v1.1.0 -m "Release 1.1.0"git push origin main --follow-tags -
Merge back. The two stabilisation fixes must reach
develop:Terminal window git switch developgit merge --no-ff release/1.1git push -
Clean up.
Terminal window git branch -d release/1.1git push origin --delete release/1.1 -
A hotfix, a week later. A customer reports a crash in 1.1.0.
developalready contains unfinished 1.2 work, so the fix cannot come from there:Terminal window git switch maingit switch -c hotfix/1.1.1git commit -am "Fix crash when the config file is empty"git switch main && git merge --no-ff hotfix/1.1.1git tag -a v1.1.1 -m "Hotfix 1.1.1"git switch develop && git merge --no-ff hotfix/1.1.1git branch -d hotfix/1.1.1
Step 7 is the scenario Git Flow exists for. With a single-branch model, shipping that fix means either shipping everything else currently on the mainline, or improvising a branch from the release tag — which is Git Flow’s hotfix branch, arrived at under pressure.
Git Flow and CI/CD
Section titled “Git Flow and CI/CD”The model predates continuous delivery, and the interaction is where most modern friction appears.
Which branch deploys where? A typical mapping:
| Branch | Environment |
|---|---|
develop | Development or integration environment |
release/* | Staging or QA environment |
main (tagged) | Production |
That is workable, but note what it implies: production deployments are driven by tags on main, not by
merges. Your pipeline needs to trigger on tags, and “deploy the latest main” is not sufficient because
main may have received a merge that is not yet the tagged release.
What runs on each branch? Feature branches and develop need the full test suite — that is where
regressions are cheapest to catch. Release branches need the full suite plus whatever slower checks you
skip elsewhere: performance tests, security scans, packaging verification.
The double merge is a pipeline concern. Because release and hotfix branches merge into two targets,
CI should verify both landed. The check is whether the release branch tip is reachable from develop:
git merge-base --is-ancestor release/1.1 develop \ && echo "stabilisation fixes reached develop" \ || echo "MISSING: release/1.1 was not merged back"Long-lived branches degrade CI signal. If develop and main diverge for weeks, a green build on
one says little about the other. This is the same divergence cost as before, appearing as reduced
confidence rather than as conflicts.
Strengths
Section titled “Strengths”Parallel version support. Maintaining 1.x while developing 2.x has a defined home. Few other models address this at all.
Stabilisation without blocking. A release can be frozen and hardened while feature work continues
on develop.
A clean production history. main is a list of releases. git log main is a changelog.
Explicit hotfix path. “Production is broken and develop is not shippable” has a documented answer
rather than an improvised one.
Well known. The vocabulary is widely understood, which has real onboarding value.
Costs and criticism
Section titled “Costs and criticism”Divergence, by design. develop and main are permanently different, and feature branches diverge
from a develop that is itself diverging. The model builds in exactly the thing that makes integration
expensive.
Two integration targets. Every change raises the question of which branch it belongs on. Answering it wrongly is easy and the consequences surface later.
Merge bookkeeping. Release and hotfix branches merging into two places is genuine ongoing work, and skipping it silently loses fixes.
A poor fit for continuous delivery. If you deploy several times a day, “main contains only
released code” and a release-branch stabilisation phase describe a process you are not running.
Ceremony without benefit. A team that deploys from develop, never uses release branches, and
supports exactly one version is maintaining two long-lived branches for nothing.
Slower feedback. A change reaches production through more stages, each adding latency between writing code and learning whether it works.
When Git Flow still makes sense
Section titled “When Git Flow still makes sense”Reasonable fits:
- Versioned software customers install. Desktop applications, mobile apps with review cycles, libraries, firmware, on-premise products.
- Several supported versions at once. If 2.1, 2.0 and 1.9 all receive fixes, you need somewhere for each to live.
- A genuine stabilisation period. Release candidates, manual QA cycles, external certification.
- Regulated environments. Where an auditable, deliberately staged path to production is required.
- Scheduled releases. Shipping monthly on a date is a different problem from shipping on merge.
Poor fits:
- Continuously deployed web services. Use GitHub Flow or trunk-based development.
- One version in production, ever.
developearns nothing. - Small teams. The coordination overhead dominates.
- Strong automated testing and fast rollback. These substitute for the stabilisation phase.
Tags carry the version, not the branch
Section titled “Tags carry the version, not the branch”Git Flow’s release branches are temporary; the durable record of a release is the tag on main.
git tag -a v1.1.0 -m "Release 1.1.0"git push origin main --follow-tagsUse annotated tags (-a) rather than lightweight ones. An annotated tag is a real object carrying a
tagger, a date and a message, and it can be signed — so it records who released what and when.
Git Objects Explained covers the difference.
--follow-tags pushes annotated tags that point at commits being pushed, which avoids the common
mistake of pushing main and forgetting the tag that names the release. Once tagged, the release
branch has done its job and can be deleted — the tag is what anyone needs to reconstruct that version.
The tooling
Section titled “The tooling”There is a widely used git-flow command-line extension that wraps the model in commands such as
git flow feature start and git flow release finish. It is a convenience layer over ordinary Git
operations, not a Git feature.
It removes the bookkeeping errors — particularly the double merge — which is a real benefit. It also hides what is happening, so people learn the commands without the model. If you adopt it, learn the underlying operations first; when something goes wrong you will be debugging Git, not the wrapper.
Adapting rather than adopting
Section titled “Adapting rather than adopting”Most teams that benefit from Git Flow benefit from part of it. The pieces are separable:
- Release branches without
develop. Branchrelease/1.4frommain, stabilise, tag, merge back. This gives you a stabilisation window without a second permanent integration branch. Often the single most useful piece. - Hotfix discipline without the rest. Branch from the released tag, fix, tag, merge forward.
- Support branches only when needed. Create
support/1.xthe day you actually have to maintain 1.x.
Taking the piece that solves your problem and leaving the rest is usually better than adopting the whole model or rejecting it wholesale.
Mental Model
Section titled “Mental Model”Git Flow separates “what we are building” from “what we have shipped”.
developis the first.mainis the second. Release branches are the airlock between them, and hotfix branches are the door that bypasses the airlock when production is on fire.
If your team has no meaningful distinction between those two states — because what you build is shipped within the hour — then the model is separating things that are not separate, and the machinery has nothing to do.
What You Learned
Section titled “What You Learned”- Git Flow uses two permanent branches,
mainanddevelop, plus feature, release and hotfix branches. maincontains only released code;developis the integration branch for the next release.- Release and hotfix branches must merge into two targets, and forgetting the second loses fixes.
- It was designed for versioned software with parallel maintenance, not continuous delivery.
- Its costs are built-in divergence, two integration targets and ongoing merge bookkeeping.
- Individual pieces — release branches, hotfix discipline — are useful independently of the whole model.
Try It Yourself
Section titled “Try It Yourself”Build the shape in a disposable repository to see the double merge concretely.
- Create a repository, commit, and tag it
v1.0.0. - Create
developfrommain. Commit two changes ondevelop. - Create
release/1.1fromdevelop. Commit a fix on it — this is stabilisation work. - Merge
release/1.1intomainwith--no-ffand tagv1.1.0. - Before merging back, run
git log --oneline developand check whether your fix is there. - Merge
release/1.1intodeveloptoo, then check again. - Run
git log --oneline --graph --alland trace both merge paths.
Step 5 is the point of the exercise: the fix is not on develop yet. In a real repository that
missing merge means the next release ships without it.
Next Lesson
Section titled “Next Lesson”Trunk-based development goes in the opposite direction: fewer branches, shorter lifetimes, and a heavy reliance on automation.