
Golang Binary Size Reduction
- 1 installs
- Updated July 29, 2026
- ehmo/golang-binary-size-reduction-skill
Reduces Go binary size safely across CLIs, daemons, Wasm, and packaged apps via linker flags, build tags, and packing, with measured before/after results.
About
Shrinks Go build artifacts through a measured workflow of stripping, build tags, CGO tuning, and packing without changing runtime behavior. A developer uses it to slim or audit Go binaries while preserving diagnostics, signing, and packaging.
- Baseline-then-measure workflow with bundled scripts
- Applies one shrink class at a time in a safe order
Golang Binary Size Reduction by the numbers
- 1 all-time installs (skills.sh)
- Ranked #79 of 98 Go skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ehmo/golang-binary-size-reduction-skill --skill golang-binary-size-reductionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | July 29, 2026 |
| Repository | ehmo/golang-binary-size-reduction-skill ↗ |
What it does
Reduces Go binary size safely across CLIs, daemons, Wasm, and packaged apps via linker flags, build tags, and packing, with measured before/after results.
Files
You are a Go binary-size reduction specialist. Optimize for measured size wins without silently changing runtime behavior, crash diagnostics, provenance, signing, or packaging guarantees.
Outcome
Always return:
1. The baseline build inputs and measurements. 2. The ranked shrink opportunities. 3. The exact commands, code changes, or packaging changes applied. 4. The before/after measurements. 5. The remaining tradeoffs and validation gaps.
Default Workflow
1. Collect build facts before changing anything. Run ./scripts/collect-build-context.sh [package]. Read references/build-inputs.md. 2. Build a reproducible baseline artifact. Run ./scripts/reproducible-build.sh -o <artifact> --pkg <package>. 3. Measure the artifact. Run ./scripts/measure-binary-size.sh <artifact>. 4. Apply changes one class at a time, in this order: 1. Release stripping. Apply -ldflags="-s -w" and -buildvcs=false. This is the single largest win for most binaries, typically 25-35% raw reduction (45-60% compressed). On Go 1.22+, -s implies -w, but keep -s -w for explicitness. 2. Inlining budget. Apply -gcflags=all=-l to disable inlining. This typically saves an additional 5-10 percentage points of raw size on top of stripping. The tradeoff is reduced runtime performance from no inlining. Acceptable for size-critical deployments; skip for latency-sensitive hot paths. Measure both. 3. Runtime and platform reductions. Default to CGO_ENABLED=0 with -tags netgo,osusergo. These three settings go together — always apply all three when disabling cgo. Check go env CGO_ENABLED and the dependency graph for cgo requirements before applying. If unsure, build with and without CGO=0 and compare sizes. CGO=0 can increase size for projects with large embedded C-backed dependencies (e.g., projects with many cloud SDK modules or C library bindings where the pure-Go replacement is larger). If the project's goreleaser or release config explicitly uses CGO_ENABLED=1 or external linking (-linkmode=external), keep CGO enabled. If CGO must stay enabled and the project compiles C source code (e.g., mattn/go-sqlite3), apply CGO_CFLAGS="-Oz" to optimize C code for size. This typically saves 1-2% raw size (~500 KB for SQLite-based projects). It has no effect when CGO only links system libraries without compiling C source. 4. Repo-specific build tags. This step is critical and must not be skipped. Search these files for -tags flags:
goreleaser.yaml/.goreleaser.ymlMakefile/Taskfile.yamlDockerfile/docker-compose.yml.github/workflows/*.ymlscripts/*.sh
Also search Go source for build constraints: grep -rn '//go:build' --include='*.go' | grep -v '_test.go' | head -40. Look for tags that gate optional heavyweight features. Examples from real projects:
WITHOUT_DOCKER(nektos/act — removes Docker/Moby client, ~15% size win)production(wailsapp/wails — removes dev server and WebSocket IPC)nodynamic(sysadminsmedia/homebox — disables SQLite dynamic extension loading)sqlite_omit_load_extension(wavetermdev/waveterm — reduces SQLite surface)
Apply any tag that disables optional features not needed for the build target. 5. Structural reductions. Remove accidental imports, split optional features into separate packages or commands, move heavyweight backends behind build tags, and shrink or externalize embedded assets. 6. Specialist tracks. Evaluate garble -tiny, UPX, TinyGo, or architecture and packaging changes only after the earlier layers are measured. UPX does not work on macOS (binaries are killed by the OS due to code signing). 5. Rebuild and remeasure after each step. Use ./scripts/compare-size-report.sh <before> <after>. 6. Stop when the next reduction changes behavior, operability, or debugging quality more than the size win justifies.
Read references/workflow.md for the full procedure.
Hard Rules
- Never treat compiler-internal hacks as normal production advice.
- Never recommend or apply
-gcflags=all=-B,-wb=false, linker patching, function-name stripping, or post-sign binary patching as standard shrink work. - Treat PGO as performance work, not binary-size work.
- Never run UPX or any other packer after signing or notarization. UPX does not work on macOS at all (SIGKILL on execution).
- Never disable cgo, switch resolver behavior, or add build tags without verifying the affected runtime behavior. Note that
CGO_ENABLED=0can increase binary size for some projects. - Never declare success from raw bytes alone. Measure raw size, compressed size, and relevant runtime behavior.
-buildid=in ldflags is redundant when-s -wis already applied.
Fast Triage
- If
-ldflags="-s -w"gives most of the win, the problem is mostly metadata. This is the common case: stripping alone typically removes 25-35% of raw size. - If stripping barely helps, inspect dependencies and package topology first.
- If compressed size barely changes, post-build packers probably will not help enough.
- If the target is Wasm or embedded, evaluate TinyGo earlier.
- If the target is macOS, UPX is not viable (kernel kills packed binaries). Prefer pre-sign changes only.
- If
plugin,reflect,text/template, orhtml/templateare present, expect dead-code elimination to be weaker. - Always check the project's release build configuration (goreleaser.yaml, Makefile, Dockerfile, CI) for existing build tags and flags. Projects often have feature-gating tags that remove large dependency subtrees.
- If
CGO_ENABLED=0increases binary size, the project links against C libraries that have smaller C implementations than their pure-Go replacements. Keep CGO enabled for these projects. - If CGO must stay enabled and C source is compiled,
CGO_CFLAGS="-Oz"is a free 1-2% win. No effect when CGO only links system libs. - Using
zig ccas CC does not reduce binary size. Zig is a cross-compilation convenience tool, not a size optimization. Benchmarks show 0-0.8% increase on native builds.
Watchlist
Always inspect the dependency graph for:
pluginreflecttext/templatehtml/templateembedtime/tzdataor-tags timetzdatanetandos/userwhen consideringCGO_ENABLED=0,netgo, orosusergo- large cloud SDKs, database drivers, telemetry stacks, and optional integrations pulled into
main
Decision Surfaces
- For branch logic and default-vs-opt-in-vs-forbidden techniques, read references/decision-tree.md.
- For validation and release gating, read references/verification.md.
- For source-backed notes, read references/sources.md.
Use the Scripts
Prefer the bundled scripts when possible:
./scripts/collect-build-context.sh./scripts/reproducible-build.sh./scripts/measure-binary-size.sh./scripts/compare-size-report.sh
They produce consistent, low-ambiguity output for agents and reduce avoidable variance across repositories.
.DS_Store
node_modules/
*.zip
Go Binary Size Reduction
Shrink Go binaries with measured tradeoffs. Covers CLIs, daemons, libraries, plugins, Wasm, and packaged apps.
Reference: https://pkg.go.dev/cmd/go
Technique priority
| # | Technique | Typical raw win | Risk |
|---|---|---|---|
| 1 | -ldflags="-s -w" (strip symbols/DWARF) | 25-35% | Weaker debugging |
| 2 | -gcflags=all=-l (disable inlining) | 5-10% additional | Reduced runtime performance |
| 3 | CGO_ENABLED=0 + -tags netgo,osusergo | 0-6% | Behavior changes; can increase size |
| 3b | CGO_CFLAGS="-Oz" (when CGO must stay on) | 1-2% | Slightly slower C code; no effect if no C source compiled |
| 4 | Project-specific build tags | 0-15% | Feature removal |
| 5 | Dependency pruning / embedded asset reduction | Varies | Source changes required |
| 6 | garble -tiny, UPX, TinyGo | Varies | Specialist tradeoffs |
Hard rules
- Never apply
-gcflags=all=-B(disables bounds checks) - Never run UPX on macOS (kernel kills packed binaries)
- Never pack or patch after code signing
- Never treat PGO as a size reduction technique
- Always measure raw size, gzip size, and runtime behavior
- Always collect build context before changing anything
Scripts
scripts/collect-build-context.sh-- Gather build factsscripts/reproducible-build.sh-- Build with stable flagsscripts/measure-binary-size.sh-- Measure artifact sizesscripts/compare-size-report.sh-- Diff two artifacts
interface:
display_name: "Go Binary Size Reduction"
short_description: "Shrink Go binaries with measured tradeoffs."
default_prompt: "Use $golang-binary-size-reduction to reduce a Go binary safely and measure the real before/after impact."
policy:
allow_implicit_invocation: true
Changelog
1.1.0 — 2026-04-01
Added
CGO_CFLAGS="-Oz"as opt-in technique for CGO-enabled projects that compile C source code (e.g., mattn/go-sqlite3). Benchmarked 1-2% raw size savings (~500 KB) on owncast, waveterm, and authelia. No effect when CGO only links system libraries.--cgo-cflagsflag inscripts/reproducible-build.sh.- Verification checklist for post-CGO_CFLAGS changes.
- Benchmark data for
CGO_CFLAGS="-Oz"andCC="zig cc"inreferences/sources.md. CC="zig cc"documented as a non-technique in the decision tree (0-0.8% size increase on native builds; it is a cross-compilation tool, not a size tool).
Changed
- Decision tree: added CGO_CFLAGS opt-in row, zig cc do-not-use row, and CGO branch guidance for C source optimization.
- Workflow Phase 4: added CGO_CFLAGS block with example command when CGO must stay enabled.
- AGENTS.md technique table: added row 3b for CGO_CFLAGS.
- SKILL.md: expanded step 3 (runtime reductions) and fast triage with CGO_CFLAGS and zig cc guidance.
1.0.0 — 2026-03-30
Initial release. Tested against 14 trending Go repositories with 36.6% average raw size reduction.
Go Binary Size Reduction Skill
Shrink Go binaries with measured tradeoffs.
SKILL.md-- Full guidelines with YAML frontmattermetadata.json-- Version, references, abstractreferences/-- Decision tree, workflow, verification, build inputs, sourcesscripts/-- Shell scripts for reproducible measurementagents/-- Agent configs (OpenAI Codex)
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"version": "1.1.0",
"organization": "ehmo",
"date": "March 2026",
"abstract": "Go binary size reduction skill. Teaches agents to shrink Go binaries through structured, measured optimization: stripping, inlining control, CGO toggling, build-tag discovery, and dependency pruning. Tested against 14 trending Go repositories with an average 36.6% raw size reduction across all builds.",
"references": [
"https://pkg.go.dev/cmd/go",
"https://go.dev/src/cmd/link/doc.go",
"https://www.datadoghq.com/blog/engineering/agent-go-binaries/",
"https://tailscale.com/blog/macos-binary-size",
"https://tailscale.com/docs/how-to/set-up-small-tailscale"
]
}
Go Binary Size Reduction Skill
Shrink Go binaries by 30-45% without breaking anything. Works with Claude Code, OpenAI Codex, and any agent that reads skill files.
Your agent collects build facts first, applies safe reductions in order of impact, measures after each change, and stops before breaking runtime behavior. No folklore flags, no cargo-culted compiler hacks.
What it does
Your agent gets a structured workflow:
1. Collect build context (Go version, CGO state, build tags, dependencies). 2. Build and measure a reproducible baseline. 3. Apply reductions one class at a time, measuring after each step. 4. Report the before/after numbers and the tradeoffs.
It includes shell scripts for reproducible measurement, a decision tree for edge cases, and hard rules that prevent the agent from recommending unsafe techniques.
Benchmark results
I tested this skill against 14 trending Go repositories from GitHub. Each repo was pinned to a specific commit, built with default flags as a baseline, then optimized by an agent using the skill. All builds passed smoke checks (the binary starts, prints help, responds to basic commands).
| Repository | Baseline | Raw reduction | gzip reduction | xz reduction |
|---|---|---|---|---|
| nektos/act | 28.0 MB | 45.9% | 61.9% | 67.5% |
| wavetermdev/waveterm | 35.4 MB | 41.6% | 60.5% | 67.4% |
| danielmiessler/Fabric | 88.0 MB | 36.9% | 56.7% | 65.9% |
| XTLS/Xray-core | 41.6 MB | 39.3% | 57.9% | 65.3% |
| henrygd/beszel | 12.1 MB | 37.2% | 56.0% | 62.4% |
| masterking32/MasterDnsVPN | 5.7 MB | 35.4% | 55.2% | 61.4% |
| mostlygeek/llama-swap | 8.8 MB | 37.7% | 55.7% | 61.7% |
| usememos/memos | 51.9 MB | 41.1% | 59.7% | 66.7% |
| sysadminsmedia/homebox | 125.4 MB | 35.5% | 53.7% | 61.6% |
| authelia/authelia | 63.4 MB | 39.2% | 58.6% | 65.4% |
| wailsapp/wails | 31.3 MB | 31.1% | 45.2% | 51.9% |
| owncast/owncast | 96.3 MB | 24.3% | 39.7% | 46.3% |
| sundowndev/phoneinfoga | 43.6 MB | 30.2% | 46.7% | 56.0% |
| supabase/cli | 124.3 MB | 37.0% | 58.2% | 66.5% |
Average raw reduction: 36.6%. Average gzip reduction: 54.7%. Average xz reduction: 61.8%.
All 14 repos passed build and smoke checks. No binary failed to start.
Reductions came from stripping debug symbols (-ldflags="-s -w"), disabling inlining (-gcflags=all=-l), toggling CGO where safe, and discovering project-specific build tags that remove optional dependency subtrees.
How it was built
The skill started as a collection of notes from the Go toolchain docs, production case studies (Datadog, Tailscale), and practical blog posts. I organized those into a decision tree and a set of hard rules about what to never do (disable bounds checks, UPX on macOS, post-sign patching).
Then I tested it. I wrote a benchmark harness that snapshots the GitHub Go trending page, pins each repo by commit SHA, builds deterministic baselines, and hands the agent a task: shrink this binary, report the numbers. The harness measures raw size, gzip size, xz size, startup time, and RSS. It runs smoke checks to confirm the binary still works.
I iterated on the skill through 13 benchmark rounds. Each round revealed gaps: projects where the agent missed feature-gating build tags, repos where CGO_ENABLED=0 made the binary bigger, edge cases around macOS signing. I patched the skill after each round and re-ran. The version in this repo is the result of that loop.
The benchmark harness lives in a separate development repo. This repo contains the tested, final skill.
Installation
npx skills add ehmo/golang-binary-size-reduction-skillOr clone this repo and point your agent at it.
Usage
Invoke it directly:
Shrink the binary for ./cmd/myapp and report the before/after sizes.Audit this Go project for binary size reduction opportunities.Apply release stripping and measure the impact.Four bundled shell scripts produce consistent output across repos:
scripts/collect-build-context.sh-- gathers Go version, CGO state, dependencies, and watchlist hitsscripts/reproducible-build.sh-- builds with stable flags and pathsscripts/measure-binary-size.sh-- reports raw, gzip, and xz sizes plus top symbolsscripts/compare-size-report.sh-- diffs two artifacts with percentage changes
Repo structure
SKILL.md -- Agent instructions with YAML frontmatter
AGENTS.md -- Quick context for agent consumption
metadata.json -- Version, references, abstract
references/
build-inputs.md -- Facts to collect before starting
decision-tree.md -- What to try, what to skip, what's forbidden
workflow.md -- Step-by-step procedure
verification.md -- How to validate the result
sources.md -- Authoritative references
scripts/
collect-build-context.sh
reproducible-build.sh
measure-binary-size.sh
compare-size-report.sh
agents/
openai.yaml -- OpenAI Codex agent configAgents
Works with any agent that reads skill files. Tested with:
- Claude Code -- reads
SKILL.mddirectly - OpenAI Codex -- uses the
agents/openai.yamlconfig
Sources
- Go build, linker, and embed docs (pkg.go.dev)
- Datadog: shrinking agent binaries (datadoghq.com/blog)
- Tailscale: small binary docs and macOS binary size work (tailscale.com)
- Filippo Valsorda, Liam Stanley, Garble, UPX, TinyGo docs
Full list in references/sources.md.
Contributing
- Keep PRs focused to one change.
- If updating rules or decision logic, explain why and include a before/after example.
- Do not add techniques to the decision tree without testing them against at least a few real repos.
License
MIT
Build Inputs
Collect these facts before proposing shrink work.
Artifact Facts
- target package or packages
- current output artifact path
- target OS and architecture
- buildmode
- whether the artifact is a CLI, daemon, plugin, shared library, Wasm module, or packaged app
Toolchain Facts
- Go version
- exact build command
GOFLAGSCGO_ENABLED- explicit
-tags,-ldflags, and-gcflags - whether the repo depends on specific Go version behavior
Runtime Facts
- whether cgo is required
- whether
pluginis required - whether
netmust use native resolver behavior - whether
os/usermust use libc-backed lookups - whether embedded timezone data is required
- whether large assets are embedded with
embed
Release Facts
- whether symbols and DWARF are needed in the shipped artifact
- whether crash symbolization happens from the shipped binary or from a retained companion artifact
- whether the target is signed or notarized
- whether the artifact is further compressed by the delivery path
- whether startup latency or cold-start time matters
Dependency Facts
- non-standard-library dependency list
- direct imports in
main - presence of heavy SDKs or optional integrations
- use of
reflect,text/template,html/template,plugin,embed,time/tzdata,net,os/user
Agent Notes
- Prefer collecting facts with
./scripts/collect-build-context.sh. - If the build already uses tags or unusual env vars, preserve them during measurement.
- If the release process signs binaries, perform any packing or patching analysis before signing, not after.
Decision Tree
Use this file to decide what to try next.
Default Techniques
These are the safest first moves.
| Technique | Use when | Notes |
|---|---|---|
-trimpath | almost always | Good hygiene and reproducibility; small size win at best |
| dependency pruning | almost always | Highest-value durable reductions |
| build-tag gating | optional features exist | Best when optional backends or cloud integrations are present |
| reduce or externalize embedded assets | embed is present | Binary size often follows asset size directly |
avoid timetzdata | timezone DB is not required in-binary | time/tzdata adds about 450 KB |
Opt-In Techniques
Use only after confirming the tradeoff is acceptable.
| Technique | Use when | Typical raw win | Risk |
|---|---|---|---|
-ldflags="-s -w" | release artifact can lose DWARF and symbol table data | 25-35% | weaker debugging and symbolization |
-buildvcs=false | provenance is captured elsewhere | <1% | weaker embedded provenance |
-gcflags=all=-l | performance regression from no inlining is acceptable | 5-10% additional | reduced runtime performance |
CGO_ENABLED=0 | cgo is not required | 0-6% (varies) | behavior changes; can increase size for some projects |
CGO_CFLAGS="-Oz" | cgo must stay enabled and C source is compiled | 1-2% | slightly slower C code execution; zero effect if CGO only links system libraries |
-tags netgo | pure-Go DNS behavior is acceptable | <1% | resolver differences |
-tags osusergo | pure-Go user/group lookup is acceptable | <1% | user lookup differences |
| project-specific tags | project has feature-gating tags (check Makefile/goreleaser) | 0-15% | feature removal |
garble -tiny | obfuscation is acceptable and crash output can be weaker | varies | harder debugging and crash analysis |
| UPX | Linux/Windows only; distribution model tolerates packers | ~50% on-disk | startup, memory, AV, signing; does not work on macOS |
| TinyGo | alternate compiler/runtime is acceptable | varies | compatibility and runtime differences |
Forbidden Techniques
Do not present these as standard production advice.
| Technique | Status | Why |
|---|---|---|
-gcflags=all=-B | forbidden | disables bounds checks |
-gcflags=all=-wb=false | forbidden | breaks GC invariants and is obsolete |
| linker patching or function-name stripping hacks | forbidden | unsupported and fragile |
| post-sign packing or patching | forbidden | breaks signatures and notarization |
| UPX on macOS | forbidden | packed binaries are killed by the kernel (SIGKILL) |
| PGO as a size tactic | do not use | can increase binary size |
-compressdwarf=false as a shrink step | do not use | increases size; only for debugger compatibility |
-ldflags="-buildid=" with -s -w | redundant | build ID is already stripped by -s -w; adds no additional savings |
CC="zig cc" as a size technique | do not use | zig cc does not reduce binary size; benchmarks show 0-0.8% increase on native builds; zig is a cross-compilation tool, not a size tool |
Repo-Specific Tag Discovery
Before applying generic flags, check the project's build infrastructure for existing feature-gating tags:
1. Read goreleaser.yaml, Makefile, Taskfile.yaml, Dockerfile, and CI workflow files. 2. Look for -tags in build commands. Projects often have tags like:
WITHOUT_DOCKER(nektos/act) — removes Docker/Moby client, ~15% additional reductionproduction(wailsapp/wails) — removes dev server codenodynamic(sysadminsmedia/homebox) — enables static SQLite buildsqlite_omit_load_extension(wavetermdev/waveterm) — reduces SQLite surface
3. Search source for //go:build and // +build constraints to find optional features. 4. These tags can remove entire dependency subtrees, producing larger wins than flag tuning.
Branches
If stripping helps a lot
The binary is metadata-heavy. Keep the change only if the artifact can lose debug data safely.
If stripping barely helps
Focus on structure:
1. prune direct imports from main 2. split optional features 3. gate integrations with build tags 4. reduce embedded assets 5. inspect plugin, reflect, and template packages
If cgo is present
Ask whether it is actually required.
If yes:
- keep cgo on
- if the project compiles C source code (e.g.,
mattn/go-sqlite3amalgamation), applyCGO_CFLAGS="-Oz"for 1-2% raw savings - focus on dependency and asset reduction
If no:
- measure a
CGO_ENABLED=0build - verify networking and user lookup behavior
If plugin is present
Assume dead-code elimination is weaker. Try to isolate plugin support into a separate binary or build-tagged path.
If embed or time/tzdata is present
Treat those bytes as intentional payload, not linker waste. Remove or move them only if the runtime contract allows it.
If the artifact is already compressed in transit
Measure gzip or xz size before considering UPX. Packers help less when the binary is already distributed inside a compressed medium.
If the target is macOS and signed
Only make size changes before signing. Re-run signing and notarization validation after the final build.
Sources
Use these sources to justify recommendations or resolve edge cases.
Highest-Trust References
- Go build docs: https://pkg.go.dev/cmd/go
- Go build constraints: https://pkg.go.dev/cmd/go
- Go linker docs: https://go.dev/src/cmd/link/doc.go
- Go reproducible builds: https://go.dev/blog/rebuild
- Go PGO docs: https://go.dev/doc/pgo
debug/buildinfo: https://pkg.go.dev/debug/buildinfoos/user: https://pkg.go.dev/os/usernet: https://pkg.go.dev/netembed: https://pkg.go.dev/embed
Strong Production Case Studies
- Datadog on shrinking agent binaries: https://www.datadoghq.com/blog/engineering/agent-go-binaries/
Best source for dependency and package-boundary wins. Treat this as the main proof that structural changes beat folklore flags.
- Tailscale small-binary docs: https://tailscale.com/docs/how-to/set-up-small-tailscale
Useful for build-tag strategy and for packer caveats.
- Tailscale macOS binary-size work: https://tailscale.com/blog/macos-binary-size
Best source for macOS-specific signing and dead-code-strip constraints.
Practical Tactic References
- Filippo Valsorda: https://words.filippo.io/shrink-your-go-binaries-with-this-one-weird-trick/
- Liam Stanley: https://liam.sh/p/shrinking-go-binaries
- Alexander Obregon: https://alexanderobregon.substack.com/p/go-binary-size-reduction
- Garble README: https://github.com/burrowers/garble
- UPX docs: https://upx.github.io/ and https://github.com/upx/upx
- TinyGo optimizing binaries: https://tinygo.org/docs/guides/optimizing-binaries/
Historical or Experimental References
- xaionaro notes: https://github.com/xaionaro/documentation/blob/master/golang/reduce-binary-size.md
Useful as a survey of old experiments, but do not treat removed or unsafe compiler flags as current advice.
- totallygamerjet smallest Go binary: https://totallygamerjet.hashnode.dev/the-smallest-go-binary-5kb
Interesting for extreme-size experiments, not for general production guidance.
- Alexey Yuzhakov: https://sibprogrammer.medium.com/go-binary-optimization-tricks-648673cc64ac
Useful for UPX measurements and tradeoffs.
- OneUptime: https://oneuptime.com/blog/post/2026-01-07-go-reduce-binary-size/view
Useful as a current blog overview, but prefer primary sources for policy-level guidance.
CGO_CFLAGS and Zig cc Benchmarks
Benchmarked April 2026 on darwin/arm64 with Go 1.26.1 and Zig 0.15.2.
CGO_CFLAGS="-Oz" (C source size optimization)
| Project | C dep | Default stripped | -Oz stripped | Raw delta |
|---|---|---|---|---|
| owncast | mattn/go-sqlite3 | 79,635,282 | 79,125,266 | -0.64% |
| waveterm | mattn/go-sqlite3 | 24,111,858 | 23,601,826 | -2.11% |
| authelia | mattn/go-sqlite3 | 46,306,946 | 45,796,946 | -1.10% |
| wails | system frameworks | 24,599,282 | 24,599,282 | 0% |
Savings are consistent (~500 KB) across mattn/go-sqlite3 projects because the SQLite amalgamation is the same C source. Zero effect when CGO only links system libraries.
CC="zig cc" (not a size technique)
| Project | Default CC | zig cc | Raw delta |
|---|---|---|---|
| act (pure Go) | 29,315,106 | 29,315,106 | 0% |
| owncast (sqlite3) | 79,635,282 | 79,797,794 | +0.20% |
| waveterm (sqlite3) | 24,111,858 | 24,299,570 | +0.78% |
| wails (system libs) | 24,599,282 | 24,599,282 | 0% |
Zig cc does not reduce binary size. On native macOS, it is clang underneath with slightly different defaults. Its value is cross-compilation convenience (single toolchain for all targets), not size.
- Zig cross-compilation overview: https://dev.to/kristoff/zig-makes-go-cross-compilation-just-work-29ho
- Uber's hermetic_cc_toolchain: https://github.com/uber/hermetic_cc_toolchain
- GoReleaser zig+cgo example: https://github.com/goreleaser/example-zig-cgo
Supplied Skill Example
- samber Go linter skill: https://github.com/samber/cc-skills-golang/blob/main/skills/golang-linter/SKILL.md
Useful as a style reference for a terse agent-facing skill layout.
Verification
A smaller binary is not a valid result until it passes both artifact checks and behavior checks.
Artifact Checks
For every before/after pair, capture:
1. raw bytes 2. gzip bytes 3. xz bytes when available 4. go version -m output 5. top symbols from go tool nm -size -sort size
Use:
./scripts/measure-binary-size.sh dist/app
./scripts/compare-size-report.sh dist/app-before dist/app-afterBehavior Checks
Run the smallest useful set of runtime checks for the target:
1. process starts successfully 2. main request path or CLI command still works 3. panic or crash output remains acceptable for the release policy 4. startup latency and RSS remain acceptable if a packer or alternate runtime was used
Special Checks
After -ldflags="-s -w"
- confirm the release process still has adequate crash-symbolization support
- keep an unstripped companion artifact if the org needs postmortem debugging
After CGO_CFLAGS="-Oz"
- verify C-code-intensive paths still meet performance requirements
- this only affects C source compilation, not system library linking
- safe for most projects since C code (e.g., SQLite) is rarely in the hot path
After CGO_ENABLED=0, netgo, or osusergo
- test DNS resolution behavior
- test user and group lookup behavior if used
- confirm certificates, proxy behavior, and environment-driven resolver behavior still match expectations
After removing timetzdata
- test timezone-sensitive paths on systems that may not have system tzdata installed
After reducing embedded assets
- test asset loading, templates, and static-file serving
After -gcflags=all=-l
- verify performance-critical paths still meet latency requirements
- benchmark hot loops if applicable
- acceptable for CLIs, build tools, and size-critical deployments
- less suitable for latency-sensitive services or inner-loop compute
After UPX
- only on Linux/Windows — macOS kills packed binaries (SIGKILL)
- test cold start
- test RSS during startup
- test antivirus, malware scanning, and unpacking workflows if relevant
- verify signing only after final packed artifact exists
After TinyGo
- test feature compatibility, runtime assumptions, and target-specific behavior
After macOS changes
- re-run codesigning checks
- re-run notarization checks if the distribution requires them
Release Gate
Ship only if:
1. the measured win is real 2. the runtime behavior is still acceptable 3. debugging and provenance tradeoffs are documented 4. signing and packaging constraints still pass
Workflow
Use this workflow unless the repo already has a stricter release pipeline.
Phase 1: Baseline
1. Identify the target package or artifact. 2. Capture the build context: ./scripts/collect-build-context.sh ./cmd/app 3. Produce a baseline artifact with stable paths: ./scripts/reproducible-build.sh -o dist/app-baseline --pkg ./cmd/app 4. Measure it: ./scripts/measure-binary-size.sh dist/app-baseline
Do not change code or flags before you have a baseline.
Phase 2: Cheap Safe Wins
Try these first. Each step typically builds on the previous:
1. -trimpath (good hygiene, small win) 2. -buildvcs=false (omit VCS stamp) 3. -ldflags="-s -w" (strip symbols and DWARF — this is the biggest single win, typically 25-35% raw reduction, 45-60% compressed) 4. -gcflags=all=-l (disable inlining — adds 5-10pp of raw reduction on top of stripping; tradeoff is reduced runtime performance)
Example:
./scripts/reproducible-build.sh \
-o dist/app-stripped \
--pkg ./cmd/app \
--omit-vcs-stamp \
--strip \
--gcflags "all=-l"
./scripts/compare-size-report.sh dist/app-baseline dist/app-strippedMeasured benchmark results (14 trending Go repos):
- Strip alone: avg 29% raw reduction
- Strip + gcflags=-l: avg 36% raw reduction
- Strip + gcflags=-l + CGO=0 + tags: avg 36-46% raw reduction (varies by repo)
If the win is large enough and the tradeoffs are acceptable, stop there.
Phase 2b: Repo-Specific Tag Discovery
Before structural changes, check the project's build infrastructure for existing feature-gating tags:
1. Read goreleaser.yaml, Makefile, Taskfile.yaml, Dockerfile, and CI workflow files. 2. Search for -tags flags in build commands. 3. Search source for //go:build constraints that gate optional features. 4. Common patterns: WITHOUT_DOCKER, production, nodynamic, sqlite_omit_load_extension. 5. These tags can remove entire dependency subtrees, often producing 5-15% additional wins.
Phase 3: Structural Reductions
If stripping and tags do not move size enough, inspect structure instead of reaching for exotic flags.
Priorities:
1. Remove unused direct imports from main. 2. Move optional features behind build tags. 3. Split heavyweight integrations into separate subcommands or binaries. 4. Reduce or externalize embedded assets. 5. Remove timetzdata unless the target truly needs embedded timezone data.
This phase usually delivers the biggest durable wins for projects that accept source changes.
Phase 4: Runtime and Platform Reductions
Default to CGO_ENABLED=0 with -tags netgo,osusergo. These three settings always go together:
CGO_ENABLED=0 go build -trimpath -tags netgo,osusergo -ldflags="-s -w" -gcflags="all=-l" -o dist/app ./cmd/appIf the resulting binary is larger than the CGO-enabled build, revert to CGO enabled. This is rare but happens when C library implementations are smaller than their pure-Go replacements.
Check for cgo requirements before applying: 1. Run go env CGO_ENABLED to see the default 2. Check the project's goreleaser.yaml or Dockerfile for explicit CGO_ENABLED=1 3. If the project uses mattn/go-sqlite3, cgo-backed crypto, or external C libraries, CGO=0 may break the build or change behavior
Do not apply CGO=0 without netgo and osusergo — without these tags, the runtime may still try to use cgo-backed resolver or user lookup functions and fail.
If CGO must stay enabled and the project compiles C source code (e.g., mattn/go-sqlite3 amalgamation), apply CGO_CFLAGS="-Oz":
CGO_ENABLED=1 CGO_CFLAGS="-Oz" go build -trimpath -ldflags="-s -w" -gcflags="all=-l" -o dist/app ./cmd/appThis optimizes the C compiler for size instead of speed. Benchmarked savings: ~500 KB / 1-2% raw on SQLite-based projects (owncast, waveterm, authelia). Has no effect when CGO only links system libraries without compiling C source (e.g., wails linking Cocoa/WebKit).
Phase 5: Specialist Tracks
Use only when the earlier phases are exhausted or the distribution model clearly supports them.
1. garble -tiny 2. UPX (Linux/Windows only — does not work on macOS, packed binaries are killed by the kernel) 3. TinyGo 4. shared-library or plugin boundary redesign
These are not default recommendations. UPX on Linux can achieve ~50% additional on-disk compression but increases startup time and RSS.
Stop Conditions
Stop when one of these is true:
1. The next reduction would weaken runtime behavior or observability too much. 2. The remaining artifact size is dominated by required dependencies or assets. 3. The requested release constraints forbid stronger shrink tactics. 4. The binary is already small enough relative to the delivery mechanism.
#!/usr/bin/env bash
set -euo pipefail
pkg="${1:-.}"
section() {
printf '== %s ==\n' "$1"
}
watchlist_pattern='^(plugin|reflect|text/template|html/template|embed|time/tzdata|os/user|net)$'
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
section "target"
printf 'package=%s\n' "$pkg"
section "go-version"
go version
section "go-env-json"
go env -json GOOS GOARCH GOAMD64 GOARM GO386 GOEXPERIMENT CGO_ENABLED CC CXX GOMOD GOWORK GOFLAGS GOROOT GOPATH GOMODCACHE
section "package-json"
go list -json "$pkg"
section "deps-non-std"
go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' "$pkg" | LC_ALL=C sort -u
section "watchlist-hits"
go list -deps -f '{{.ImportPath}}' "$pkg" | LC_ALL=C sort -u > "$tmp"
if command -v rg >/dev/null 2>&1; then
rg -n "$watchlist_pattern" "$tmp" || true
else
grep -nE "$watchlist_pattern" "$tmp" || true
fi
#!/usr/bin/env bash
set -euo pipefail
if (($# != 2)); then
printf 'usage: compare-size-report.sh <before-artifact> <after-artifact>\n' >&2
exit 64
fi
before="$1"
after="$2"
for artifact in "$before" "$after"; do
if [[ ! -f "$artifact" ]]; then
printf 'artifact not found: %s\n' "$artifact" >&2
exit 66
fi
done
file_bytes() {
if [[ "$(uname -s)" == "Darwin" ]]; then
stat -f '%z' "$1"
else
stat -c '%s' "$1"
fi
}
gzip_bytes() {
gzip -n -9 -c "$1" | wc -c | tr -d ' '
}
xz_bytes() {
if command -v xz >/dev/null 2>&1; then
xz -9e -c "$1" | wc -c | tr -d ' '
else
printf '0\n'
fi
}
delta_pct() {
awk -v before="$1" -v after="$2" 'BEGIN {
if (before == 0) {
print "0.00"
exit
}
printf "%.2f", ((after - before) / before) * 100
}'
}
section() {
printf '== %s ==\n' "$1"
}
before_bytes="$(file_bytes "$before")"
after_bytes="$(file_bytes "$after")"
before_gzip="$(gzip_bytes "$before")"
after_gzip="$(gzip_bytes "$after")"
before_xz="$(xz_bytes "$before")"
after_xz="$(xz_bytes "$after")"
section "summary"
printf 'before=%s\n' "$before"
printf 'after=%s\n' "$after"
printf 'before_bytes=%s\n' "$before_bytes"
printf 'after_bytes=%s\n' "$after_bytes"
printf 'delta_bytes=%s\n' "$((after_bytes - before_bytes))"
printf 'delta_pct=%s\n' "$(delta_pct "$before_bytes" "$after_bytes")"
printf 'before_gzip_bytes=%s\n' "$before_gzip"
printf 'after_gzip_bytes=%s\n' "$after_gzip"
printf 'delta_gzip_bytes=%s\n' "$((after_gzip - before_gzip))"
printf 'delta_gzip_pct=%s\n' "$(delta_pct "$before_gzip" "$after_gzip")"
printf 'before_xz_bytes=%s\n' "$before_xz"
printf 'after_xz_bytes=%s\n' "$after_xz"
printf 'delta_xz_bytes=%s\n' "$((after_xz - before_xz))"
printf 'delta_xz_pct=%s\n' "$(delta_pct "$before_xz" "$after_xz")"
section "go-version-before"
go version -m "$before" 2>&1 || true
section "go-version-after"
go version -m "$after" 2>&1 || true
#!/usr/bin/env bash
set -euo pipefail
if (($# != 1)); then
printf 'usage: measure-binary-size.sh <artifact>\n' >&2
exit 64
fi
artifact="$1"
if [[ ! -f "$artifact" ]]; then
printf 'artifact not found: %s\n' "$artifact" >&2
exit 66
fi
section() {
printf '== %s ==\n' "$1"
}
file_bytes() {
if [[ "$(uname -s)" == "Darwin" ]]; then
stat -f '%z' "$1"
else
stat -c '%s' "$1"
fi
}
sha256_file() {
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
sha256sum "$1" | awk '{print $1}'
fi
}
gzip_bytes() {
gzip -n -9 -c "$1" | wc -c | tr -d ' '
}
xz_bytes() {
if command -v xz >/dev/null 2>&1; then
xz -9e -c "$1" | wc -c | tr -d ' '
else
printf 'unavailable\n'
fi
}
section "summary"
printf 'path=%s\n' "$artifact"
printf 'bytes=%s\n' "$(file_bytes "$artifact")"
printf 'sha256=%s\n' "$(sha256_file "$artifact")"
printf 'gzip_bytes=%s\n' "$(gzip_bytes "$artifact")"
printf 'xz_bytes=%s\n' "$(xz_bytes "$artifact")"
section "file"
file "$artifact" 2>/dev/null || true
section "go-version-m"
go version -m "$artifact" 2>&1 || true
section "top-symbols"
go tool nm -size -sort size "$artifact" 2>/dev/null | head -n 40 || true
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
usage: reproducible-build.sh -o <artifact> [options]
Options:
--pkg <package> package to build (default: .)
-o, --output <path> output artifact path
--strip add -ldflags="-s -w"
--omit-vcs-stamp add -buildvcs=false
--disable-cgo set CGO_ENABLED=0 for the build
--tags <list> pass -tags
--buildmode <mode> pass -buildmode
--ldflags <flags> append linker flags
--gcflags <flags> pass gcflags
--cgo-cflags <flags> set CGO_CFLAGS (e.g., -Oz for size optimization)
-- <extra args...> extra go build args before the package
EOF
}
pkg="."
out=""
strip=0
omit_vcs=0
disable_cgo=0
tags=""
buildmode=""
ldflags=""
gcflags=""
cgo_cflags=""
extra_args=()
while (($# > 0)); do
case "$1" in
--pkg)
pkg="$2"
shift 2
;;
-o|--output)
out="$2"
shift 2
;;
--strip)
strip=1
shift
;;
--omit-vcs-stamp)
omit_vcs=1
shift
;;
--disable-cgo)
disable_cgo=1
shift
;;
--tags)
tags="$2"
shift 2
;;
--buildmode)
buildmode="$2"
shift 2
;;
--ldflags)
ldflags="$2"
shift 2
;;
--gcflags)
gcflags="$2"
shift 2
;;
--cgo-cflags)
cgo_cflags="$2"
shift 2
;;
--)
shift
extra_args=("$@")
break
;;
-h|--help)
usage
exit 0
;;
*)
printf 'unknown argument: %s\n' "$1" >&2
usage >&2
exit 64
;;
esac
done
if [[ -z "$out" ]]; then
printf 'missing required output path\n' >&2
usage >&2
exit 64
fi
mkdir -p "$(dirname "$out")"
go_args=(build -trimpath -o "$out")
if [[ "$omit_vcs" -eq 1 ]]; then
go_args+=(-buildvcs=false)
fi
if [[ -n "$tags" ]]; then
go_args+=(-tags "$tags")
fi
if [[ -n "$buildmode" ]]; then
go_args+=(-buildmode "$buildmode")
fi
final_ldflags="$ldflags"
if [[ "$strip" -eq 1 ]]; then
if [[ -n "$final_ldflags" ]]; then
final_ldflags+=" "
fi
final_ldflags+="-s -w"
fi
if [[ -n "$final_ldflags" ]]; then
go_args+=(-ldflags "$final_ldflags")
fi
if [[ -n "$gcflags" ]]; then
go_args+=(-gcflags "$gcflags")
fi
if ((${#extra_args[@]} > 0)); then
go_args+=("${extra_args[@]}")
fi
go_args+=("$pkg")
effective_cgo="inherit"
if [[ "$disable_cgo" -eq 1 ]]; then
effective_cgo="0"
fi
{
printf 'CGO_ENABLED=%s\n' "$effective_cgo"
if [[ -n "$cgo_cflags" ]]; then
printf 'CGO_CFLAGS=%s\n' "$cgo_cflags"
fi
printf 'command='
printf '%q ' go "${go_args[@]}"
printf '\n'
} >&2
build_env=()
if [[ "$disable_cgo" -eq 1 ]]; then
build_env+=(CGO_ENABLED=0)
fi
if [[ -n "$cgo_cflags" ]]; then
build_env+=(CGO_CFLAGS="$cgo_cflags")
fi
if ((${#build_env[@]} > 0)); then
env "${build_env[@]}" go "${go_args[@]}"
else
go "${go_args[@]}"
fi