Packfiles and How Git Stays Small

A snapshot-per-commit model sounds like it should balloon a repository to enormous size. It doesn't — a project with a decade of history often clones smaller than a single day's build artifacts. The trick is packfiles: Git periodically takes its loose objects, deltas the similar ones against each other, and compresses the result. This is where "Git stores snapshots" and "Git repositories are tiny" stop seeming contradictory.

We said in the first post that every commit records a full snapshot, and that content-addressing keeps this cheap by sharing identical objects. That’s true, but it only deduplicates identical content. What about a file edited a thousand times — a thousand similar but distinct blobs? Storing each in full would be wasteful. Packfiles are how Git solves the remaining problem.

Loose objects vs. packed objects

When you make a commit, Git writes each new object as a loose object: one file per object under .git/objects/, its content zlib-compressed, named by its hash (the first two hex characters become a directory, the rest the filename):

.git/objects/a1/b2c3d4e5f6...

Loose objects are simple and fast to write — perfect for the moment of committing. But they have two costs at scale. Each is compressed in isolation, so a file and its slightly-edited successor are two separately-compressed full copies with no sharing between them. And thousands of tiny files stress the filesystem. Loose storage is a good write format and a poor long-term format.

The delta idea

The insight behind packfiles: similar objects can be stored as differences from one another. If blob B is a small edit of blob A, Git can store A in full and store B as “take A, then apply these changes” — a delta. The delta is tiny compared to a full copy of B.

This is different from the snapshot-vs-diff distinction of post 1. Conceptually, every commit still references a complete tree — the object model is snapshots. Packfiles are a storage-layer optimization underneath that model: they choose, invisibly, to physically store some objects as deltas against others. The logical model (snapshots, content-addressed) is unchanged; only the bytes on disk get cleverer. You never see deltas in git cat-file; Git reconstructs the full object on demand.

Two subtleties make it work well: - Deltas can chain. B is a delta of A, C is a delta of B, and so on — a delta chain. Git bounds the chain length so reconstruction doesn’t require walking hundreds of steps. - Deltas point newest-to-oldest, by size. Git generally stores the larger (usually newer) object in full and deltas the smaller against it, and it delta-compresses in the direction that keeps recent history fast to access.

The packfile

Git bundles many objects into a single packfile.git/objects/pack/pack-<hash>.pack — with a companion index file pack-<hash>.idx. The pack holds the objects (many as deltas, then everything zlib-compressed on top); the .idx is a lookup table mapping each object’s hash to its byte offset in the pack, so Git can find any object without scanning.

To find an object, Git checks loose storage, then consults each pack’s .idx. To read a packed object, it seeks to the offset and, if the object is a delta, walks its base chain applying deltas until it has the full content. All of this is invisible: git show, git log, git checkout reconstruct whole objects transparently. The packfile is purely a storage representation.

When packing happens: gc

You don’t pack manually. Git runs git gc (“garbage collect”) automatically when loose objects accumulate — after commits, merges, and especially after a fetch. gc does two related jobs:

The two-week grace period and reflog protection are deliberate: they give you a window to recover “lost” commits before gc truly erases them. Reachability decides what’s kept; the grace period decides when the unreachable is swept.

Why clones are small and fast

Packfiles aren’t just local housekeeping — they’re also the transfer format. When you clone or fetch, the server doesn’t send thousands of loose objects; it builds a packfile of exactly the objects you need and streams that. This means:

So the apparent paradox — “Git keeps a full snapshot per commit” yet “Git repos are small and clone fast” — resolves cleanly. The model is snapshots for conceptual simplicity and integrity; the storage and transport are delta-compressed packfiles for efficiency. Two layers, each optimized for its job, and the seam between them is invisible in daily use.

Key takeaways

Further reading

Sources & References

Delta compression, gc, and the transfer format