Last Updated: May 22, 2026
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.
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.
go mod vendorgo 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.
vendor/modules.txtThe 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/".
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.21vendor/modules.txt consistent with go.modvendor/ directorythen 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:
| Value | Effect |
|---|---|
-mod=vendor | Force vendor mode. Build from vendor/ and fail if it's missing or inconsistent. |
-mod=mod | Force module-cache mode. Ignore vendor/ entirely, even if it exists. Download what's needed. |
-mod=readonly | Use 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.
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 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 CheckRelated 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.
Vendoring isn't free, so it should answer a specific need. These are the cases where it's appropriate:
proxy.golang.org internally) is heavier and more complex.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.go.mod.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.
The flip side is just as important. Vendoring costs you something every time, so don't do it by reflex:
vendor/ and reviewing large diffs. The signal-to-noise ratio drops fast.git blame gets cluttered.The decision matrix:
| Aspect | Vendored | Not Vendored |
|---|---|---|
| Build needs network? | No (after go mod vendor once) | Yes (until module cache is warm) |
| Survives yanked module? | Yes | No |
| Survives proxy outage? | Yes | No (unless cache warm) |
| Repo size | Larger (deps in git) | Smaller |
| PR diffs for dep changes | Large (full code diff) | Small (one-line in go.mod) |
| Reviewable supply chain | Yes (deps in code review) | Limited (only go.sum hash changes) |
| Maintenance burden | Higher (re-run go mod vendor after every change) | Lower |
| Reproducibility years later | Strong | Depends on proxy / cache availability |
| Best for | Enterprise, air-gapped, regulated | Most other cases |
There's no universal answer. The right choice depends on where the project runs and who needs to trust the build.
vendor/ in Sync with go.modThe 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.
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.