Git, CI/CD & GitOpsXVII · ReflogExpiry
Reflog expiry and gc — git reflog expire, gc.reflogExpire, and the choreography with garbage collection
What you'll learn
- Explain how git reflog expire prunes entries by age and reachability
- Configure gc.reflogExpire and gc.reflogExpireUnreachable to tune retention per clone
- Recognise the choreography between reflog expiry, git gc, and git gc --prune=<time>
- Configure gc.pruneWorktrees for clones with multiple worktrees
- Apply the audit-clone discipline (gc.reflogExpire never, core.logAllRefUpdates true)
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Reflog expiry is the silent killer of recovery attempts. The
commit was reachable only via a reflog entry, the entry has
expired, and the orphan is now a candidate for garbage
collection. By the time the engineer returns to recover, the
recovery path is closed. The discipline is to understand the
expiry mechanics — git reflog expire, gc.reflogExpire,
gc.reflogExpireUnreachable — and to configure them
deliberately on clones that need long retention.
git reflog expire
git reflog expire prunes reflog entries according to a
policy. The default policy is taken from the clone’s
configuration (gc.reflogExpire for reachable entries,
gc.reflogExpireUnreachable for unreachable entries), but
the command accepts explicit overrides.
# Prune entries using the configured policy (run by git gc automatically)
git reflog expire --all
# Prune all entries immediately
git reflog expire --expire=now --all
# Prune only unreachable entries immediately
git reflog expire --expire-unreachable=now --all
# Override retention to 7 days for this run only
git reflog expire --expire=7.days --all
# Expire entries older than a specific date
git reflog expire --expire=2026-08-01.00:00:00 --all
# Expire a specific ref's entries
git reflog expire --expire=now refs/heads/feature/x
The --all flag applies the expiry to every ref’s reflog.
Without it, only the entries that are already considered
unreachable are pruned (the more conservative default).
git gc always passes --all when it runs the expire step.
The two retention knobs
The retention policy has two knobs because reachability has two tiers:
- Reachable entries name a commit that is reachable from
some current ref. The default retention is 90 days
(
gc.reflogExpire=90.days). - Unreachable entries name a commit that is not reachable
from any current ref — typically a commit on a deleted
branch, a commit on a detached HEAD that was never promoted,
or a commit orphaned by
--hardreset or rebase. The default retention is 30 days (gc.reflogExpireUnreachable=30.days).
# Inspect the current policy
git config gc.reflogExpire
# 90.days
git config gc.reflogExpireUnreachable
# 30.days
# Tune the policy per clone
git config gc.reflogExpire 180.days
git config gc.reflogExpireUnreachable 60.days
# Disable expiry entirely (audit-clone discipline)
git -C /path/to/audit-clone config gc.reflogExpire never
git -C /path/to/audit-clone config gc.reflogExpireUnreachable never
The never value is a literal token Git recognises: it
disables expiry for the affected scope. The cost is disk
space (reflog entries accumulate indefinitely), and the
benefit is the forensic answer to “what was every ref
pointing at on date X?” for any date in the clone’s lifetime.
flowchart LR
A["reflog entry"] --> B{"reachable?"}
B -- yes --> C["retention: gc.reflogExpire\n(default 90 days)"]
B -- no --> D["retention: gc.reflogExpireUnreachable\n(default 30 days)"]
C --> E["expired -> prune candidate"]
D --> E
The choreography with git gc
git gc runs three operations in sequence: reflog expire,
prune, repack. The reflog expire step is what removes
reflog entries; the prune step is what removes the commit
objects those entries were the only path to.
git gc
# 1. git reflog expire --all (prunes expired reflog entries)
# 2. git prune --expire=<now> (removes objects with no path)
# 3. git repack (packs remaining loose objects into packfiles)
The choreography matters because each step has its own expiry policy. A typical chain:
- A commit
Xis onfeature/x. The branch reflog hasfeature/x@{0}: commit: X. git branch -D feature/xdeletes the branch.Xis now reachable only via the branch reflog entry.git gcruns. The reflog expire step seesXas reachable (the reflog still names it) and applies the reachable retention (90 days). After 90 days, the entry expires andXbecomes unreachable.- The next
git gcruns. The reflog expire step seesXas unreachable and applies the unreachable retention (30 days). After 30 more days, the entry expires again. - The prune step removes
Xfrom the object store.
The total retention is 90 days (reachable) + 30 days
(unreachable) = 120 days from the branch deletion. The
audit-clone discipline (gc.reflogExpireUnreachable never)
short-circuits this and retains the entry indefinitely.
git gc —prune=<time>
The --prune flag sets the prune step’s expiry to a
specific time. Objects last referenced before that time and
not reachable from any ref or reflog entry are removed.
# Aggressive: prune everything not currently reachable
git gc --prune=now
# Conservative: only prune objects unreachable for >14 days
git gc --prune=14.days
# Inspect what would be pruned without actually pruning
git gc --prune=now --dry-run
The default prune retention is 2 weeks (gc.pruneExpire,
default 2.weeks.ago). The 2-week grace period gives a
window in which an object that has been orphaned but is
still in the prune-step grace period is recoverable via
git fsck --unreachable. After the grace period, the prune
step removes it.
gc.pruneWorktrees
gc.pruneWorktrees (default true) controls whether git gc removes worktree metadata for worktrees that no longer
exist on disk. A worktree is the linked-checkout feature:
git worktree add <path> <branch> creates a second working
tree sharing the same .git directory.
git config gc.pruneWorktrees true
# git gc removes .git/worktrees/<name> for worktrees whose
# working directory no longer exists
The setting matters for clones with many short-lived
worktrees (CI runners that create and destroy worktrees per
build). With pruneWorktrees=true, the metadata is cleaned
up automatically; with false, the metadata accumulates
and the reflog of the orphaned worktree’s branch may survive
longer than expected.
The audit-clone discipline
A clone intended for forensic work — an audit trail, a post-incident review, a regulatory archive — should be configured with indefinite retention:
git clone $REPO /path/to/audit-clone
cd /path/to/audit-clone
# Reflog retention
git config gc.reflogExpire never
git config gc.reflogExpireUnreachable never
# Tag reflog
git config core.logAllRefUpdates true
# Repack to consolidate (optional)
git gc --prune=now
The cost is disk space: the reflog grows over time, and the object store retains every commit ever made (because every commit is reachable from some reflog entry indefinitely). The benefit is the answer to “what was the state of HEAD on date X, and what was the OID of commit Y on that date?” for any date in the clone’s lifetime.
flowchart LR
A["audit-clone"] --> B["gc.reflogExpire = never"]
A --> C["gc.reflogExpireUnreachable = never"]
A --> D["core.logAllRefUpdates = true"]
A --> E["daily git reflog --all -> immutable log"]
The diagram shows the configuration of an audit clone. The
immutable log (git reflog --all output, captured daily and
archived) is the artefact that survives even if the clone
itself is lost.
Production discipline
- Know your clone’s retention.
git config gc.reflogExpireandgc.reflogExpireUnreachablereveal the policy. - For audit clones, set both to
never. The disk cost is small relative to the forensic benefit. - Avoid
git gc --prune=nowon production clones. The command is useful for testing and for clones with no forensic value; it is destructive on clones whose reflog is the recovery path for past mistakes. - Schedule
git gcto run regularly but not aggressively. The defaultgc.auto(6700 loose objects) is a reasonable threshold; CI runners may want a more frequent schedule. - Document the retention policy in the team runbook. A junior engineer who does not know the clone’s retention may push commits that are recoverable for 90 days but expects them to be recoverable forever.
Cross-course references
- Git, CI/CD & GitOps — Part V (Branches, Refs and HEAD) —
Part V lesson 6 introduced the relationship between
reflog expiry and garbage collection at the foundations
level. This lesson is the advanced follow-up on the
choreography between
git reflog expire,git gc, andgit gc --prune=<time>. - Git, CI/CD & GitOps — Part XV (Reset) — the recovery
window for a
--hardreset is bounded bygc.reflogExpireandgc.reflogExpireUnreachable. - GitOps with Argo CD — Part IV (SyncPhases) — the
GitOps controller configures
gc.reflogExpire neveron its clone so that every commit it has ever synced remains reachable by reflog entry, and runsgit gcon a schedule that does not prune reflog entries.
Quiz
Knowledge check · 4 questions
Q1. Which sequence does `git gc` run internally?
Q2. Setting `gc.reflogExpireUnreachable never` on a clone prevents `git gc` from ever pruning unreachable reflog entries, which makes the commit objects those entries reference indefinitely recoverable.
Q3. What is the audit-clone discipline, and which three configuration options does it set?
Q4. Diagnose why a recovery attempt has failed and recommend the configuration changes that would have made it succeed.
An engineer ran `git reset --hard HEAD~2` 60 days ago. Today, the engineer runs `git reflog` to recover the pre-reset OID and finds that the entry is no longer present. The clone's `gc.reflogExpire` is at the default value. The recovery recipe fails because the OID is gone from the reflog and the commit object is no longer reachable.
Passing score: 75%. Answers are checked in this browser.