git push rejected, non-fast-forward
Fix "rejected non-fast-forward": run git pull --rebase then git push. Use --force-with-lease only to overwrite on purpose, and recover lost commits via reflog.
! [rejected] main -> main (non-fast-forward)
Incident Overview
Fix "rejected non-fast-forward": run git pull --rebase then git push. Use --force-with-lease only to overwrite on purpose, and recover lost commits via reflog.
easy fix·7 min read
By Vishvesh Patel·DevOps Engineer·Updated 16 September 2026
! [rejected] main -> main (non-fast-forward) error: failed to push some refs to 'github.com:acme/orders-api.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes before pushing again. ! [rejected] main -> main (fetch first)
Do this first3 steps
Run these in order. Each one tells you what its output means before you change anything.
• See exactly how far apart the branches are
The two numbers are commits you are missing and commits you would add. Anything non-zero on the left is precisely why the push was refused.
• Integrate their work, then push
Replays your commits on top of theirs so history stays linear. Resolve any conflict, git rebase --continue, then push normally.
• Only if you deliberately rewrote a branch that is yours alone
--force-with-lease refuses if anyone pushed since your last fetch, which plain --force will happily destroy. Never force-push a shared branch such as main.
Git is refusing the push because accepting it would delete commits that exist on the remote and not in your branch. That refusal is a safety feature, and it is almost always protecting someone else's work.
The instinct to reach for --force is what turns a two-minute inconvenience into an afternoon of recovery for a colleague.
git fetch origin && git rev-list --left-right --count origin/main...HEAD
See exactly what you would be discarding
Before doing anything, find out what is actually on the other side:
< is yours, > is theirs. Those two > commits are what a force-push would remove.
If the first number is zero, something else is wrong. See the stale-copy section below.
git fetch origin git log --oneline --graph --left-right HEAD...origin/main
The normal fix: integrate, then push
Two ways, and the choice affects your history rather than the outcome.
Rebase replays your commits on top of theirs, keeping history linear:
Merge creates a merge commit joining the two lines:
Rebase is the usual choice for your own feature work, because the result reads as though you started from current code. Merge is correct when others are working on the same branch, since rebasing shared commits strands them.
Setting rebase as the default stops a busy repository filling with "Merge branch 'main' of…" commits:
If the rebase hits conflicts, resolve each, git add the files, and git rebase --continue. git rebase --abort returns you to exactly where you started, at any point.
git pull --rebase origin main git push origin main
When you legitimately need to overwrite
There is a real case: you rebased or amended your own feature branch, so your local history is intentionally different and the remote copy is the stale one.
Use --force-with-lease, never --force. The difference is what happens when someone else has pushed since you last fetched:
That rejection is the lease working. Fetch, look at what arrived, and decide deliberately.
Make it the default so the safe form is the one your fingers learn:
Never force-push a shared branch. On main, or any branch others have pulled, everyone who has it is stranded on commits that no longer exist, and their next pull produces a mess that takes real time to untangle. Protect those branches at the host so the question cannot arise.
git push --force-with-lease origin feature/user-search
"fetch first" is the same thing, earlier
This variant means your local origin/main is out of date. Git can see the remote has moved but has not downloaded the commits yet. git fetch then proceeds as above. Functionally it is the same situation as non-fast-forward.
! [rejected] main -> main (fetch first)
A stale local copy of the remote
Occasionally the push is rejected while git log origin/main..HEAD shows nothing missing. That means your remote-tracking branch is wrong:
Two causes. Your copy of origin/main is stale, which fetch fixes. Or the branch was rewritten or deleted and recreated upstream, in which case your origin/main points at commits that no longer exist there.
That keeps deleted upstream branches from lingering locally, which removes a whole class of confusion.
git fetch --prune origin git remote -v
Recovering commits a force-push destroyed
If someone has already forced over your work, it is very likely recoverable, locally.
The reflog records every position HEAD has held on your machine and keeps entries for around 90 days, so anything you committed is still reachable even when no branch points at it.
Two caveats. The reflog is local, so it only helps on a machine that had the commits. And work that was never committed is genuinely gone, which is the strongest argument for committing early, even messily.
On the server side, GitHub and GitLab retain unreachable commits for a period and their reflog or activity API can sometimes recover them, though it is far less convenient than having a local copy.
git reflog
Preventing it
Fetch before you start work, not when you are ready to push. Discovering divergence after an hour of work is the expensive version.
Keep feature branches short. The longer a branch lives, the more main moves and the worse every integration gets.
Protect main with a rule requiring pull requests and forbidding force-push, so the dangerous option is unavailable rather than merely discouraged.
Set pull.rebase true and fetch.prune true globally. Both remove recurring friction.
A checklist
Frequently Asked Questions
What does "non-fast-forward" mean in Git?
A fast-forward is when the remote branch's tip is an ancestor of yours, so the remote pointer can simply move forward and nothing is lost. Non-fast-forward means that is not true: the remote has commits your branch does not contain, so accepting your push would remove them. Git refuses rather than silently discarding history. The fix is to bring those commits into your branch with git pull --rebase or git pull, then push.
Should I use git push --force to fix this?
Almost never, and not as a first response. Force-pushing overwrites the remote branch, deleting the commits Git was protecting, which on a shared branch strands everyone who has pulled it. Integrate the remote changes instead. The legitimate case is your own feature branch after an intentional rebase or amend, and even then use --force-with-lease, which refuses if someone else has pushed since your last fetch.
What is the difference between --force and --force-with-lease?
--force overwrites the remote branch unconditionally. --force-with-lease first checks that the remote is still where you last saw it, and refuses with "stale info" if someone else has pushed in the meantime. So the destructive outcome, silently deleting a colleague's commits, becomes an error message instead. There is no situation where plain --force is preferable; alias --force-with-lease so it is what you type by default.
What is the difference between "non-fast-forward" and "fetch first"?
Effectively nothing. "fetch first" appears when your local copy of the remote branch is out of date, so Git knows the remote has moved but has not downloaded the new commits. "non-fast-forward" appears when it has them and can see the divergence. Both mean the remote holds commits you do not, and both are resolved the same way: fetch, integrate with rebase or merge, then push.
My push is rejected but git log shows nothing missing. Why?
Your remote-tracking branch is stale, so you are comparing against an old snapshot of the remote. Run git fetch --prune origin and check again. The other possibility is that the upstream branch was rewritten or deleted and recreated, leaving your origin/main pointing at commits that no longer exist there. Setting fetch.prune true globally keeps this class of confusion from recurring.
Can I recover commits that someone force-pushed over?
Usually, if you have a machine that had them. git reflog lists every position HEAD has held locally and retains entries for about 90 days, so git switch -c recovered <sha> brings the work back even though no branch points at it any more. The reflog is local, so it only helps where the commits once existed. Uncommitted work is not recoverable at all, which is the practical argument for committing early and often, even on messy work in progress.
Reference and practice
Learn the underlying concept
Other Git errors
Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.
Get notified about new tutorials and features.
• Roadmaps
• Interview Prep
• Certifications
• Company
© 2026 DevOpsLesson. All rights reserved.
Need to practice this scenario?
Simulate this error condition and practice recovery commands safely in our in-browser terminal.