Git: push rejected — Updates were rejected because the remote contains work
Hi, it's BlueByte. You run git push and, instead of uploading your commits, Git stops with ! [rejected] main -> main (fetch first) and a paragraph that says the remote contains work you don't have. Nothing is broken and no commits are lost — Git is refusing to overwrite history it can see and you can't. The symptom is a push that ends in Updates were rejected with either (fetch first) or (non-fast-forward) in the reject line. We'll walk through what those two variants mean, why the push was refused, how to bring the two histories together safely, and how to keep it from happening.
What "(fetch first)" and "(non-fast-forward)" are telling you
The full rejection reads like this:
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:acme/app.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.Git only accepts a push when your branch is a straight continuation of the remote branch — a fast-forward. When the remote has moved on, your commit is no longer a direct descendant of its tip, so Git refuses rather than throw away the remote's commits. The two hint words tell you which case you're in: (fetch first) means your local branch simply hasn't seen the remote's new commits yet, and (non-fast-forward) means the two histories have actually diverged — both sides have commits the other doesn't.
The two situations that produce a rejected push
A non-fast-forward reject comes from one of these:
- Someone pushed after you last synced. A teammate or a CI job added commits to the same branch. Your history is behind, not wrong — you just need to catch up.
- You rewrote your local history. A
git rebase,git commit --amend, orgit resetchanged commits you'd already pushed, so your branch and the remote's now disagree about the past.
The first is the common one and has a clean fix. The second needs care, because the safe fix and the dangerous one look almost identical.
See how far the histories have diverged before you act
Don't force anything yet — first fetch the remote state and measure the gap:
git fetch
git statusYour branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.That line is the whole diagnosis: you have 1 commit the remote lacks, and it has 2 you lack. To see them side by side:
git log --oneline --graph --left-right HEAD...origin/mainCommits marked < are yours; > are the remote's. If every commit is > and none is <, you're simply behind — a plain git pull will fast-forward. If both appear, the branches diverged and you'll create a merge or a rebase.
The normal fix: bring the remote work in, then push
For the everyday case — someone pushed ahead of you — pull their commits, replay yours on top, and push:
git pull --rebase
git pushSuccessfully rebased and updated refs/heads/main.
...
To github.com:acme/app.git
3f2a1b0..9c8d7e6 main -> maingit pull --rebase fetches the remote commits and re-applies yours after them, so history stays linear and the push now fast-forwards. Plain git pull (a merge) works too, but leaves a merge commit. If the rebase reports a conflict, resolve the files, git add them, and run git rebase --continue.
When you rewrote history on purpose: --force-with-lease, never --force
If the divergence is because you intended to rewrite your own pushed commits — squashing a messy branch before review, say — pulling them back would undo your cleanup. Here you overwrite the remote, but do it with a safety net:
git push --force-with-lease--force-with-lease only overwrites the remote if it still points where you last saw it — so if a teammate pushed in the meantime, the push fails instead of deleting their commit. Plain git push --force skips that check and can silently erase work someone else pushed; reach for --force-with-lease every time, and never force a shared branch like main without agreement.
A real case: a teammate pushed while you were working
You finish a fix, commit it, and git push is rejected with (fetch first) and Updates were rejected because the remote contains work that you do not have. You run git fetch and git status, which reports the branch diverged by 1 and 1. Since you didn't rewrite anything, this is the normal case: you run git pull --rebase, Git replays your one commit on top of your teammate's, and git push succeeds — the reject line is gone and both commits are on the remote, in order.
Confirm the push actually landed
Check that local and remote agree:
git statusYour branch is up to date with 'origin/main'.
nothing to commit, working tree cleanup to date with 'origin/main' means the push fast-forwarded cleanly. git log --oneline -3 should show your commit at the tip alongside the remote's, with no (fetch first) on the next push.
Keep rejected pushes from surprising you
Fetch before you start work and again before you push, so you rarely sit behind the remote. Set git config --global pull.rebase true so git pull rebases by default and keeps history linear instead of littering merge commits. On shared branches, prefer a pull request over pushing directly, and if you must rewrite history, always use --force-with-lease so you can't clobber a colleague. Small, frequent pushes diverge less than one big push after a day of work.
How this differs from a protected-branch or "src refspec" rejection
A non-fast-forward reject is about history shape — your branch isn't a descendant of the remote's, and fetching fixes it. Two other rejections look similar but aren't: protected branch hook declined or GH006 means the server policy forbids the push regardless of history — you need a pull request, and no amount of pulling helps. And error: src refspec main does not match any means the branch doesn't exist locally at all — usually a typo or an empty repo with nothing committed yet. If the message names a hook or a refspec rather than "remote contains work," it isn't a non-fast-forward and git pull won't fix it.
Related questions
What's the difference between (fetch first) and (non-fast-forward)?
Both are the same class of rejection. (fetch first) means your local branch hasn't seen the remote's newer commits yet; (non-fast-forward) means the histories have already diverged. Run git fetch and git status to tell which one you're in.
Can I just use git push --force to get past it?
It works, but --force skips the safety check and can silently delete commits a teammate pushed. Use git push --force-with-lease instead — it only overwrites if the remote is still where you last saw it, and fails otherwise.
git pull --rebase gives me a conflict.
Open the conflicted files, resolve the markers, git add them, then run git rebase --continue. If you'd rather back out and rethink, git rebase --abort returns you to where you started.
Should I use merge or rebase to integrate the remote work?
Both are valid. Rebase (git pull --rebase) keeps a linear history; merge (plain git pull) preserves the exact topology with a merge commit. Set your default with git config --global pull.rebase true if you prefer linear.
My reject says 'protected branch', not 'remote contains work'. Same thing?
No. A protected-branch rejection is server policy — the push is refused no matter how up to date you are, and pulling won't help. Open a pull request for that branch instead.
References
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
More in this category
Git: fatal: detected dubious ownership in repository
Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.
Kubernetes: Internal error occurred: failed calling webhook
An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)
The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.
Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang
The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.