Git

git push rejected, non-fast-forward

The remote has commits you do not. Why force-push is the wrong instinct, what --force-with-lease does differently, and how to recover work you have already overwritten.

easy fix7 min read

git. The error
! [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.

  1. 1

    See exactly how far apart the branches are

    git fetch origin && git rev-list --left-right --count origin/main...HEAD

    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.

  2. 2

    Integrate their work, then push

    git pull --rebase origin main

    Replays your commits on top of theirs so history stays linear. Resolve any conflict, git rebase --continue, then push normally.

  3. 3

    Only if you deliberately rewrote a branch that is yours alone

    git push --force-with-lease origin feature/user-search

    --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.

All 9 sections

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.

See exactly what you would be discarding

Before doing anything, find out what is actually on the other side:

git fetch origin
git log --oneline --graph --left-right HEAD...origin/main
< a3f2c1d  Add retry to payment client        ← yours only
< 7b8c9d0  Fix typo in README
> f4e5d6c  Bump axios to 1.7.4                ← theirs only
> 2a1b3c4  Add rate limiting to /api/login

< is yours, > is theirs. Those two > commits are what a force-push would remove.

git rev-list --count HEAD..origin/main      # commits you are missing
git rev-list --count origin/main..HEAD      # commits you would add

If the first number is zero, something else is wrong. See the stale-copy section below.

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:

git pull --rebase origin main
git push origin main

Merge creates a merge commit joining the two lines:

git pull origin main
git push origin main

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:

git config --global pull.rebase true

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.

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.

git push --force-with-lease origin feature/user-search

Use --force-with-lease, never --force. The difference is what happens when someone else has pushed since you last fetched:

--force--force-with-lease
Remote unchanged since your fetchPushesPushes
Someone else pushedSilently destroys their commitsRefused
! [rejected]  feature/user-search -> feature/user-search (stale info)

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:

git config --global alias.pushf 'push --force-with-lease'

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.

"fetch first" is the same thing, earlier

! [rejected]        main -> main (fetch first)

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.

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:

git fetch --prune origin
git remote -v

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.

git config --global fetch.prune true

That keeps deleted upstream branches from lingering locally, which removes a whole class of confusion.

Recovering commits a force-push destroyed

If someone has already forced over your work, it is very likely recoverable, locally.

git reflog
a3f2c1d HEAD@{0}: pull: Fast-forward
7b8c9d0 HEAD@{1}: commit: Add retry to payment client
f4e5d6c HEAD@{2}: commit: Fix typo in README
git switch -c recovered 7b8c9d0

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.

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

  1. git fetch origin. Get the current state.
  2. git log --oneline --left-right HEAD...origin/main. See both sides.
  3. Their commits are wanted → git pull --rebase then push.
  4. Your own branch, deliberately rewritten → git push --force-with-lease.
  5. --force-with-lease rejected → someone pushed; fetch and look again.
  6. Never force-push main or any shared branch.
  7. Nothing appears missing → git fetch --prune; your remote copy is stale.
  8. Work already overwritten → git reflog and branch from the lost commit.

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.

Learn the underlying concept

Other Git errors