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

MeasureBefore tag importAfter tag import
Content pages6,4006,400
Tags1401,920
Total pages rendered8,10031,700
Full build11 s94 s
Peak memory1.1 GB3.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.

Number of rendered pages by kind after the tag import Horizontal bars. Paginator pages for terms 21,400. Content pages 6,400. Term list pages 1,920. Section and home list pages 1,980. What Hugo rendered after the tag import Term paginator pages 21400 Content pages 6400 Section and home lists 1980 Term list pages 1920
Counts from Hugo's build summary and the output directory; term pages and their pagination were 74 percent of all pages.

Step 1: Measure the Templates

hugo --templateMetrics --templateMetricsHints --gc > metrics.txt
head -20 metrics.txt

The top of the table, sorted by cumulative time:

TemplateCumulativeAverageCount
_default/term.html61.2 s2.9 ms21,400
partials/related.html38.7 s1.4 ms27,600
partials/summary.html22.1 s0.11 ms196,000
_default/single.html7.8 s1.2 ms6,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.

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.

Distribution of tags by how many posts use them Vertical bars. 1,040 tags were used on one post. 380 on two posts. 220 on three to nine posts. 180 on ten to forty-nine posts. 100 on fifty or more posts. Tags by number of posts 1040 1 post 380 2 posts 220 3–9 posts 180 10–49 100 50+
The 1,420 tags with one or two posts were merged or dropped; they had produced 4,800 paginator pages.

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/**"
Full build time before and after each change Paired bars. Removing related content from list templates took the build from 94 to 58 seconds. Cutting the tag long tail took it from 58 to 27 seconds. Coarser pagination and lighter summaries took it from 27 to 15 seconds. Disabling the series taxonomy pages took it from 15 to 13 seconds. Build time after each change Before change After change No related content on lists 94 s 58 s Cut tag long tail 58 s 27 s Pagination 30, light summaries 27 s 15 s Series pages not rendered 15 s 13 s
Each row is cumulative; hyperfine, five runs, same 4-core CI runner.

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

MeasureAfter importAfter fixesTool
Full build94 s13 shyperfine, 5 runs
Pages rendered31,70010,900Hugo build summary
Tags1,920410hugo list all + script
Peak memory3.8 GB1.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 .Summary in high-volume lists. Use .Description or 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.

Filed underFixesHugo