AlgoMaster Logo

Vendoring

Last Updated: May 22, 2026

Low Priority
9 min read

Vendoring is the practice of copying every module dependency your project uses into a local vendor/ directory that lives inside your repository and gets committed to source control. Instead of relying on the module cache or a network fetch from a proxy, the Go toolchain builds straight from those local copies. This lesson covers the go mod vendor command, the structure of the vendor/ tree, what vendor/modules.txt contains, the -mod flag, default Go behavior when a vendor/ directory is present, and when (and when not) to vendor.

A Short Bit of History

Before Go modules existed, vendoring was the standard way to pin dependencies. Tools like dep and glide would write all transitive dependencies into a vendor/ directory next to your code, and go build would automatically find them there. When modules landed in Go 1.11, the module cache and go.sum took over most of vendoring's job. Vendoring didn't go away, though. It became opt-in, and it remains a first-class feature of the toolchain because some teams still need it for reasons that the module cache alone can't satisfy.

The thing to understand: vendoring used to be the default mechanism, now it's a specialized tool. You pick it up when you have a real reason, not by habit.

The Command: go mod vendor

go mod vendor is the one command you need to know. Run it at the root of a module (next to go.mod) and it walks your dependency graph, downloads any missing modules into the module cache, then copies the parts you actually use into a vendor/ directory under your project.

Consider an e-commerce store module that uses two external dependencies: github.com/google/uuid for generating order IDs and github.com/shopspring/decimal for handling product prices without floating-point drift. Here's what a typical session looks like:

After go mod vendor runs, the vendor/ directory mirrors the layout of the module cache, but only for the modules your code (and your dependencies) actually import.

The tree is grouped by hosting domain (github.com), then organization (google, shopspring), then module name (uuid, decimal). License files come along for the ride. Test files don't. The vendor/ directory is meant for building, not for re-running upstream tests, so files like *_test.go and packages that aren't reachable from your import graph get skipped.

What's Inside vendor/modules.txt

The most important file in the whole vendor/ tree is vendor/modules.txt. It's a plain text manifest that the Go toolchain uses to verify that the contents of vendor/ match what go.mod says the project depends on. Without it, the build won't trust the directory.

Here's a realistic snippet from an e-commerce module that depends on uuid and decimal:

Each block follows the same shape. The first line starting with # names a module path and the exact version that was vendored. The second line starting with ## carries module metadata: explicit means your go.mod directly requires this module (as opposed to it being a transitive dependency pulled in by something else), and go 1.19 records the Go version declared by that module's own go.mod. The lines that follow list the packages from that module which were copied into vendor/.

Look at the third entry. golang.org/x/sys shows up even if your code never imports it, because uuid or decimal probably do. Only the specific subpackage actually used (unix) gets copied. The rest of golang.org/x/sys (which has many subpackages for different operating systems) stays out of the vendor tree.

vendor/modules.txt is small but load-bearing. If it disappears or stops matching go.mod, your build breaks. Don't edit it by hand. Always regenerate it with go mod vendor.

The Go toolchain compares vendor/modules.txt against go.mod on every build. If they disagree (you added a new import, you bumped a version, you removed a dependency), the build refuses to proceed and tells you to re-run go mod vendor. This consistency check is what makes vendor builds trustworthy: there's no quiet drift between "what go.mod says" and "what's actually in vendor/".

How the Build Knows to Use vendor/

Go 1.14 changed the default behavior in a way that's easy to miss. If a vendor/ directory exists at the module root and it's consistent with go.mod and your declared go version is at least 1.14, the go build command automatically uses vendor mode. You don't need to pass a flag. The build will not consult the module cache or the network, and it will not respect GOPROXY. It reads sources straight out of vendor/.

Concretely, if you check out a repository that contains:

  • go.mod declaring go 1.21
  • vendor/modules.txt consistent with go.mod
  • A populated vendor/ directory

then go build./... will build entirely from the local files. No network. No surprises. This is the property that makes vendoring useful for air-gapped builds and reproducible CI runs.

The -mod flag overrides the default. There are three values:

ValueEffect
-mod=vendorForce vendor mode. Build from vendor/ and fail if it's missing or inconsistent.
-mod=modForce module-cache mode. Ignore vendor/ entirely, even if it exists. Download what's needed.
-mod=readonlyUse the module cache but refuse to update go.mod or download new modules. Useful in CI.

If you don't pass -mod, Go picks: vendor mode when vendor/ is present and consistent, module-cache mode otherwise. Most projects never need to set the flag explicitly. CI pipelines sometimes set -mod=vendor defensively, to fail loudly if someone forgot to run go mod vendor after editing imports.

That's the canonical CI command for a vendored project. It guarantees the build uses local sources and nothing else.

Build Flow: With and Without Vendor

It helps to see what changes between the two modes:

In module-cache mode (the top path), the toolchain reads go.mod, looks up each required module in the local module cache, and on a cache miss fetches the missing version from proxy.golang.org (or whatever GOPROXY points to). In vendor mode (the bottom path), the toolchain reads go.mod and vendor/modules.txt, confirms they agree, then reads sources out of vendor/ directly. The network is never touched, and the module cache is never consulted. That's the property that makes vendor builds reproducible even months later, on a machine that has never seen the relevant module versions.

A Worked Example

A small e-commerce program that uses both vendored dependencies. The program builds a couple of catalog entries with stable IDs and exact prices:

(The first eight characters of each UUID will be different on your machine; UUIDs are random.)

After writing this code, the workflow is:

From this point on, anyone who clones the repository can run go build without an internet connection, on a fresh machine, on the day the proxy is down, and the build will still produce the same binary.

go mod verify: The Consistency Check

Related to vendoring (but broader) is go mod verify. The command checks that every module in your local module cache still matches the checksum recorded in go.sum. It catches the case where something in the cache has been corrupted or tampered with.

go mod verify operates on the module cache, not on vendor/. So in a purely vendored build, it doesn't verify what gets compiled. The check that protects vendored code is the consistency check between vendor/modules.txt and go.mod, plus the fact that vendor/ itself is committed to git and reviewed like any other code. If you want belt-and-braces, run go mod verify before go mod vendor: that way the bytes the cache hands you (and that vendor then copies) are confirmed to match go.sum.

go mod verify reads every file in the module cache and hashes it. On a large dependency tree, it's not instant. For CI, run it once per build, not per package.

When to Vendor

Vendoring isn't free, so it should answer a specific need. These are the cases where it's appropriate:

  • Air-gapped or restricted networks. If your build servers don't have outbound internet access, vendoring is the only way to get external dependencies onto them. The alternative (mirroring proxy.golang.org internally) is heavier and more complex.
  • Reproducible CI even if modules are yanked or deleted. A module owner can delete a tag, retract a version, or take their repository private. go.sum protects you from getting different bytes back, but it doesn't protect you from getting no bytes at all. Vendored code keeps building regardless.
  • Supply-chain pinning that's reviewable in code review. With vendoring, every byte of every dependency is in git. Dependency updates show up as diffs that reviewers can read, scan, and approve. Without vendoring, the only visible change is a one-line version bump in go.mod.
  • Compliance and audit requirements. Some industries (defense, finance, healthcare) require every line of code in a shipped product to be traceable through source control. Vendoring satisfies that requirement without needing extra tooling.
  • Organizational policy. Some companies simply mandate vendoring across all Go projects. If you work somewhere that does, the decision is already made.

The most common professional reason is the first one. Enterprise build environments often run in locked-down VPCs, sometimes air-gapped entirely from the public internet, sometimes restricted to a vetted internal proxy. Vendoring lets a Go project build inside that environment without negotiating special access for proxy.golang.org or maintaining an internal module mirror.

When Not to Vendor

The flip side is just as important. Vendoring costs you something every time, so don't do it by reflex:

  • Small or solo projects. If you're the only developer and you have a working internet connection, the module cache is faster and simpler.
  • Fast-moving dependencies. Projects that update dependencies weekly will spend a lot of time regenerating vendor/ and reviewing large diffs. The signal-to-noise ratio drops fast.
  • Repos that value slim git history. A vendored repository can be 5-10x larger than its un-vendored counterpart. Clones are slower, history is noisier, and git blame gets cluttered.
  • Public open-source libraries. Library users want flexibility to pick their own versions of transitive dependencies. Vendoring a library forces consumers to live with your choices.

Vendoring vs No-Vendoring: The Trade-off

The decision matrix:

AspectVendoredNot Vendored
Build needs network?No (after go mod vendor once)Yes (until module cache is warm)
Survives yanked module?YesNo
Survives proxy outage?YesNo (unless cache warm)
Repo sizeLarger (deps in git)Smaller
PR diffs for dep changesLarge (full code diff)Small (one-line in go.mod)
Reviewable supply chainYes (deps in code review)Limited (only go.sum hash changes)
Maintenance burdenHigher (re-run go mod vendor after every change)Lower
Reproducibility years laterStrongDepends on proxy / cache availability
Best forEnterprise, air-gapped, regulatedMost other cases

There's no universal answer. The right choice depends on where the project runs and who needs to trust the build.

Keeping vendor/ in Sync with go.mod

The maintenance burden is real, and it's the main reason vendoring fell out of fashion for everyday projects. Anytime you change a dependency, you have to update both go.mod and vendor/. The workflow is:

If you skip the go mod vendor step, your next build will fail with the inconsistent-vendoring error from earlier. That failure is annoying but it's the safety net. It's worse to have a vendor directory that doesn't match go.mod, because then your builds might pass locally (using the module cache) and fail in CI (using vendor mode), or vice versa.

The order matters too. Run go mod tidy before go mod vendor, not after. tidy prunes unused dependencies and adds missing ones, normalizing go.mod. vendor then snapshots that normalized state into the filesystem. Reversing the order means vendor copies things that tidy is about to remove.

Vendoring and Go Version Mismatches

Each module declares its own minimum Go version in its go.mod (the go 1.x line). When you vendor a module, that declaration gets preserved in vendor/modules.txt (the ## explicit; go 1.x line). If you later try to build with an older Go toolchain than one of your vendored dependencies requires, the build fails with a version mismatch error.

This is the same behavior you'd get without vendoring; vendor mode just makes the mismatch more visible because the requirement is sitting right there in vendor/modules.txt. The fix is either to upgrade the toolchain or to find a version of the dependency that supports your toolchain. Don't try to edit vendor/modules.txt to lie about the requirement; the toolchain will still read each module's vendored go.mod (yes, those are vendored too) and enforce the constraint.

A related gotcha: if your team uses go.work for multi-module development and you also vendor, the workspace mode and vendor mode interact in non-obvious ways.