bloaty McBloatface: Why Is My Binary So Big, in Actual Detail

2026-07-06

Google's Josh Haberman released bloaty years ago and it still isn't in most engineers' toolboxes. If you've ever tried to answer "why did my binary just grow 400KB after that refactor?" with size, nm --size-sort, or a handful of objdump -h incantations, you already know the pain: flat lists, no attribution, no way to see the forest. Bloaty is a hierarchical profiler for binary size — ELF, Mach-O, PE, WebAssembly, and raw .a archives.

The default view already earns its keep:

$ bloaty ./server
    FILE SIZE        VM SIZE
 --------------  --------------
  33.7%  4.02Mi  40.1%  4.02Mi    .text
  17.8%  2.12Mi  21.1%  2.12Mi    .rodata
  12.4%  1.48Mi   0.0%       0    .debug_info
   9.1%  1.09Mi   0.0%       0    .debug_str
   ...
 100.0%  11.9Mi 100.0%  10.0Mi    TOTAL

File size vs. VM size in one table — instantly tells you what ships to disk vs. what gets paged in. But the real trick is -d (dimensions), which nests groupings:

# What compile units contribute the most .text?
$ bloaty -d compileunits ./server -n 10

# Drill down: which symbols inside which compile units?
$ bloaty -d compileunits,symbols ./server -n 5

# Group by section, then by source template/inline expansion
$ bloaty -d sections,inlines ./server

The compileunits and inlines data comes from DWARF, so it maps bloat back to the actual .cc file and line that spawned it. That's the piece nm can't give you: attribution across template instantiations, inlined helpers, and generated code.

Diff mode is where bloaty pays for itself in code review:

$ bloaty -d symbols ./server.new -- ./server.old
    FILE SIZE        VM SIZE
 --------------  --------------
   +8.4% +142Ki   +8.4% +142Ki    [200 Others]
  +11.2%  +89Ki  +11.2%  +89Ki    absl::flat_hash_map<...>::rehash
   -4.1%  -12Ki   -4.1%  -12Ki    old_json_parser
   ...
   +2.1% +251Ki   +2.1% +251Ki    TOTAL

Every entry is signed. You can immediately see whether that "small" dependency bump dragged in 300KB of hashmap templates, and which symbol grew.

Stripped binaries with a debug companion:

$ bloaty --debug-file=server.debug ./server-stripped -d compileunits

You get DWARF-quality attribution on the production binary you actually ship. This is how you audit a container image's payload without shipping symbols to prod.

WebAssembly: bloaty groks .wasm natively. If you're chasing "why is my Rust WASM 800KB?", bloaty -d sections,symbols app.wasm beats every browser DevTools panel.

Domains you can pivot on: sections, segments, symbols, compileunits, inlines, armembers (inside .a), rawranges. Combine two or three with commas for a tree. Add -s vm or -s file to sort by whichever size you actually care about; add --source-filter=regex to focus on one library.

Where it beats the alternatives: size gives you six numbers per file. nm --size-sort gives you a flat symbol list with no compilation-unit context. objdump shows sections but no attribution. Bloaty is the only tool that answers "where did this KB come from, all the way back to the .cc file" without you writing shell to correlate three outputs.

Key Takeaway: When a binary grows and you need to know why, bloaty's hierarchical dimensions and diff mode turn "10 MB of mystery" into a signed, source-attributed tree in one command.

All newsletters