Fixing Hugo Builds That Slow Down After Adding Taxonomies
Hugo builds are fast enough that people rarely think about them — until a content change makes one ten times slower. Adding a taxonomy is one of the most common causes. A blog adds a topics taxonomy, an importer copies across 1,800 WordPress tags, or a docs site starts tagging pages by product, and the build that took 9 seconds now takes 70. Nothing is broken, and Hugo is doing exactly what it was asked: rendering a list page for every term, paginating it, and running each term template's logic for every member.
This guide fixes a slowdown on a 6,400-post engineering blog built with Hugo 0.139, where importing tags from an old platform took the full build from 11 seconds to 94. It shows how to find the expensive templates, the four changes that brought the build to 13 seconds, and how to stop it happening again. It belongs to Hugo Build Times for Large Repositories.
Symptoms
| Measure | Before tag import | After tag import |
|---|---|---|
| Content pages | 6,400 | 6,400 |
| Tags | 140 | 1,920 |
| Total pages rendered | 8,100 | 31,700 |
| Full build | 11 s | 94 s |
| Peak memory | 1.1 GB | 3.8 GB |
The page count is the first clue. Hugo's build summary reports "Pages" and "Paginator pages"; the jump from 8,100 to 31,700 came almost entirely from term pages and their pagination.
Step 1: Measure the Templates
hugo --templateMetrics --templateMetricsHints --gc > metrics.txt
head -20 metrics.txt
The top of the table, sorted by cumulative time:
| Template | Cumulative | Average | Count |
|---|---|---|---|
_default/term.html | 61.2 s | 2.9 ms | 21,400 |
partials/related.html | 38.7 s | 1.4 ms | 27,600 |
partials/summary.html | 22.1 s | 0.11 ms | 196,000 |
_default/single.html | 7.8 s | 1.2 ms | 6,400 |
(Cumulative times overlap, because partials run inside other templates.) Three facts stand out. The term template ran 21,400 times — once per paginator page. The related-posts partial ran 27,600 times, not 6,400, because the term template called it too. And the summary partial ran nearly 200,000 times, ten summaries per paginator page.
Step 2: Stop Computing Related Content on List Pages
The term template had been copied from the single template years earlier and still included the "Related posts" sidebar. On a term page, it computed related content for the term page itself, which made no sense to readers and cost 1.4 ms per call. Removing it from list templates cut 38 seconds. Where a sidebar is genuinely needed on list pages, partialCached with a key that does not vary per page renders it once:
{{ partialCached "sidebar-popular.html" . "global" }}
Step 3: Cut the Tag Long Tail
1,420 of the 1,920 tags were used on fewer than three posts — artefacts of the old platform, where writers had tagged freely. Their pages were thin (one or two posts) and made up most of the term pages. Two changes:
- Tags with fewer than three posts were merged into broader tags or dropped, using a script that rewrote front matter and produced a redirect list for the removed tag pages.
- A rule in the contributing guide, enforced by a CI check, requires new tags to come from
data/tags.yaml.
That took the taxonomy from 1,920 terms to 410.
Step 4: Paginate Term Pages More Coarsely
Term pages paginated at 10 posts per page, inherited from the home page. For the ten largest tags, that meant hundreds of paginator pages each — "golang" alone had 96. Term pages now paginate at 30 with lighter summaries (title, date and the description field, no rendered excerpt):
[pagination]
pagerSize = 30
The summary partial on term pages reads .Description instead of calling .Summary, which forces Hugo to render the content to extract the first paragraph. That one change made the summary partial roughly 8× cheaper per call.
Step 5: Do Not Render What Nobody Visits
Analytics showed that paginator pages beyond page 3 of any term received almost no traffic — 0.2% of term page views. Hugo has no setting to cap pagination depth, but the term template can link only to the first three pages plus an "all posts in this tag" archive page that lists titles only, rendered once per term as a separate output format. Deep paginator pages still exist (Hugo renders every page of a paginator), but with 30 per page and lighter summaries their cost was small enough to leave.
For taxonomies that do not need pages at all, disable them outright. The blog's series taxonomy is only shown on posts and never browsed:
[taxonomies]
tag = "tags"
series = "series"
disableKinds = []
[[cascade]]
[cascade.build]
render = "never"
list = "local"
[cascade._target]
path = "/series/**"
Why the Dev Server Felt Fine
The slowdown went unnoticed for two weeks because hugo server stayed quick. The dev server renders pages on demand and re-renders only what a change affects, so writers editing a post never paid for 21,400 paginator pages. Only CI, which runs full builds, showed the problem — and CI times were not watched. The lesson for Hugo sites is that local experience says little about full build cost; the page count in the build summary is the number to watch, and it is cheap to log on every CI run.
Keeping It From Coming Back
Taxonomy growth is gradual, so the regression comes back slowly unless something watches it. The CI job records the page count and build time from hugo --printPathWarnings --gc on every main-branch build and fails a pull request that increases rendered pages by more than 5% without a corresponding increase in content pages — a signal that a template or taxonomy change, not content, is multiplying output. It has fired twice since: once for a new authors taxonomy with pagination at 10, and once for a template that accidentally added a paginator to tag pages' RSS output.
Measured Impact
| Measure | After import | After fixes | Tool |
|---|---|---|---|
| Full build | 94 s | 13 s | hyperfine, 5 runs |
| Pages rendered | 31,700 | 10,900 | Hugo build summary |
| Tags | 1,920 | 410 | hugo list all + script |
| Peak memory | 3.8 GB | 1.3 GB | /usr/bin/time -v |
| Term page views lost | — | 0 (redirected) | Analytics, 30 days |
Removed tag pages were redirected to the tag they were merged into, so readers following old links landed somewhere sensible, as in SEO-Safe Pagination and Tag Pages.
Pitfalls & Rollback
- Copying single-page partials into list templates. Related content and other per-page computations do not belong on term pages.
- Calling
.Summaryin high-volume lists. Use.Descriptionor a front matter field. - Unbounded free-form tags. Keep a list and enforce it.
- Dropping tag pages without redirects. They may have inbound links.
- Rollback: each change is a template or config edit; the tag merge is a content commit with its redirect file, revertible together.
Conclusion
Taxonomies slow Hugo builds by multiplying list pages, and list templates that do per-page work multiply them again. On a 6,400-post blog, removing related content from term templates, merging 1,420 near-empty tags, paginating more coarsely with lighter summaries, and not rendering a browse-free taxonomy took the build from 94 seconds back to 13.
FAQ
Why do taxonomies make Hugo builds slower?
Each taxonomy term gets its own list page, and each list page renders its member pages' summaries, often with pagination. A site with 6,000 posts and 1,800 tags can generate more term pages than content pages. If each term page does expensive work, such as computing related content or rendering full summaries, the build time grows with terms times members rather than with the number of posts.
How do I find which template is slow?
Run hugo with the templateMetrics and templateMetricsHints flags. Hugo prints cumulative and average duration per template and the number of times each was called, and hints at which templates would benefit from partialCached. Term and list templates near the top of that table are the usual cause after adding taxonomies.
Should I limit the number of tags?
Usually yes, for readers as much as for build time. Tags used on one or two posts make thin pages with little value and many of them. Merging rare tags into broader ones or excluding them from term page generation removes work and improves the site. Hugo cannot cap terms automatically, so the cleanup belongs in content or a build script.
Can I disable term pages but keep the taxonomy?
Yes. Setting build options in the front matter of a term's _index.md, or using disableKinds for taxonomy or term kinds in the site config, stops Hugo rendering those pages while keeping the taxonomy data available to templates, for example to show tags on posts without linking to a page per tag.
Related
- Parent: Hugo Build Times for Large Repositories — the main causes of slow Hugo builds.
- Profiling Hugo Templates With Template Metrics — reading the metrics table in depth.
- Hugo partialCached for Faster Builds — caching shared partials.
- Reducing Hugo Memory Usage on CI Runners — the memory side of the same problem.
- SEO-Safe Pagination and Tag Pages — what tag pages are worth to search.