Fix Git "Updates Were Rejected" on Push
Your push bounces because the remote moved on without you. Here is how to catch up cleanly, when to rebase versus merge, and when a force-push is actually safe.
Key takeaways
- Git push is rejected as non-fast-forward when the remote branch has commits your local branch does not.
- Run git fetch, then git pull --rebase (or a merge pull), resolve any conflicts, and push again.
- Only force-push branches you own, and then use git push --force-with-lease, never plain --force.
On this page
Git rejects a push as "non-fast-forward" when the remote branch has commits your local branch doesn't, so pushing would erase them. The fix: run git pull --rebase origin main (or a plain merge pull), resolve any conflicts, then git push. Force-push only your own branches, with --force-with-lease.
You finish a change, run git push, and Git slaps you down:
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
Nothing is broken. This is Git protecting you. The remote branch has commits your local branch has never seen, and Git refuses to overwrite them with your version. The fix is to bring those commits into your history first, then push. The interesting part is how you bring them in, and the one case where you're allowed to shove your history over the top.
What "non-fast-forward" actually means
A fast-forward push is the easy case: your local branch is the remote branch plus a few extra commits on top. Git can move the remote pointer forward in a straight line, no history rewriting, no merging. Everyone's happy.
Non-fast-forward means the histories have split. Someone pushed to origin/main after you last pulled. Now your branch and the remote branch share a common ancestor but each has commits the other lacks. Git can't move the pointer in a straight line anymore, so it stops.
You'll see this wearing a few different hats:
Updates were rejected because the remote contains work that you do not have locallyUpdates were rejected because the tip of your current branch is behind its remote counterparthint: (e.g., 'git pull ...') before pushing again- On newer clients, a plain
fetch first
Same root cause every time. The remote moved. You didn't.
Step one: look before you leap
Before you pull anything, see what you're dealing with.
git fetch origin
git log --oneline --graph HEAD origin/main
fetch updates your view of the remote without touching your working branch. The graph shows you the split: your commits on one line, theirs on another, a shared ancestor below. Now you decide how to reconcile them.
Merge vs rebase: pick your history
Two ways to integrate remote work. They produce different histories, and the difference matters more than most people admit.
git pull (merge) fetches the remote commits and ties the two lines back together with a merge commit.
git pull origin main
git push origin main
Your history keeps a visible fork-and-join. It's honest about what happened: two people worked in parallel and their work was joined. The downside is a log full of Merge branch 'main' of origin commits that carry no real information.
git pull --rebase replays your local commits on top of the remote ones, one at a time, as if you'd written them after everyone else.
git pull --rebase origin main
git push origin main
The result is a straight line. No merge bubble, no noise. This is my default for the everyday "I have two small commits, someone else pushed while I was working" situation. The catch: rebasing rewrites your local commit hashes, so never rebase commits you've already pushed and shared. On unpushed local work it's completely safe.
Since Git 2.33.1, a plain git pull on diverged branches stops and asks you to choose --rebase or --no-rebase. If you want rebase as the standing behavior for pulls, set it once:
git config --global pull.rebase true
When the pull hits a conflict
Either approach can stop on a conflict when your changes and theirs touched the same lines. The workflow is the same idea, slightly different verbs.
With a merge pull, fix the marked files, then:
git add path/to/file
git commit
With a rebase pull, fix the files, then continue the replay:
git add path/to/file
git rebase --continue
Rebase stops at each conflicting commit, so you may resolve, continue, resolve, continue a few times. If it turns into a mess and you want out, git rebase --abort puts you back exactly where you started, no harm done. That escape hatch is why I don't fear rebase.
Once the pull is clean, git push fast-forwards and you're done.
The force-push case
Sometimes you want your version to win. You cleaned up a feature branch with an interactive rebase, squashed some noise, amended a commit message. Now your local history and the remote history genuinely disagree, and pulling would just re-merge the junk you deleted. This is the legitimate home of force-push.
Never reach for plain --force:
git push --force origin my-feature # don't
Plain force overwrites the remote no matter what's there. If a teammate pushed to that branch in the last ten minutes, you just deleted their commits and won't know until they ask where their work went.
Use --force-with-lease instead:
git push --force-with-lease origin my-feature
--force-with-lease only overwrites the remote if it still points where you last saw it. If someone pushed in the meantime, the lease is broken and Git rejects the push, handing you back the exact same rejected message from the top of this post. That rejection is the feature. It means you're about to clobber someone, and now you know to fetch and look first.
One caveat from the git push docs: a background git fetch (some IDEs run one) updates your remote-tracking ref and silently defeats the lease. Add --force-if-includes (Git 2.30+) so Git also checks that you actually integrated the remote tip.
The rule I hold without exception: force-push only branches that are yours. Your feature branch, your experiment, a PR branch nobody else builds on. Never main, never a shared release branch, never a branch three people are cutting work from.
Protected branches
On GitHub, GitLab, or Bitbucket, the branches that matter are usually protected, and force-push is disabled server-side. Even --force-with-lease bounces:
remote: error: GH006: Protected branch update failed for refs/heads/main.
This is the platform saving you from yourself. The correct move on a protected branch is never to force anything. Pull with rebase, resolve, push normally, or open a pull request and let the merge happen through review. If you find yourself wanting to force-push main, stop. That urge is the bug.
Questions people ask
Why does Git say "the tip of your current branch is behind its remote counterpart"?
Your local branch is missing commits that exist on the remote, usually because someone pushed after your last pull. Git refuses the push so it doesn't discard their work. Run git pull --rebase, resolve any conflicts, and push again.
Is it safe to use git push --force to fix a rejected push?
Only on a branch nobody else pushes to, and even then prefer git push --force-with-lease, which refuses to overwrite commits you haven't seen. On main or any shared branch, never force: integrate the remote work and push normally.
Should I use git pull or git pull --rebase?
Use git pull --rebase for small unpushed local commits; you get linear history with no merge noise. Use a merge pull when you want the parallel work recorded, or when your commits are already shared, since rebasing rewrites their hashes.
The call we'd make
Ninety percent of the time this error means one thing: fetch, then git pull --rebase, resolve any conflict, push. Straight-line history, no drama. Reach for a merge pull only when you actually want the fork recorded, or when the branch policy forbids rewriting. Force-push lives on your own branches and nowhere else, and when you do it, it's --force-with-lease every single time so Git can still stop you before you overwrite someone's afternoon. For the wider set of Git failures that land on you mid-sprint, our Git troubleshooting guide covers the rest.
Sources
Azure Workload Identity Federation Without Secrets
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Terraform vs Pulumi — HCL vs Real Code
Terraform's declarative HCL and Pulumi's real programming languages solve the same problem in very different ways, and the right pick depends on your team.
More from DevOps
Explore more articles in this category
GitHub Copilot CLI Sandboxing Is GA: What Actually Changed
GitHub made local sandboxing generally available for Copilot CLI on October 7, so the default now restricts the agent instead of trusting it.
The Tensorlake npm Compromise Targeted Your AI Tool Configs
A compromised npm release went after cloud credentials as expected, and local AI coding-assistant config files, which most teams never treated as secrets.
GitLab's New Rate Limits: What to Fix Before Oct 19
GitLab is capping unauthenticated API calls at 60 an hour starting October 19, and the preview windows land before most teams will have noticed.
You might have missed
Evergreen posts worth revisiting.