Right, let’s talk about how Hugo actually builds your site without taking a geological epoch to do it. You’ve probably run hugo and been pleasantly surprised by how fast it is, especially compared to… well, pretty much every other static site generator. The secret sauce isn’t magic; it’s a disciplined, aggressive use of concurrency via Go’s goroutines.

Think of your site as a giant pile of pages that all need to be rendered. A naive approach would be to grind through them one at a time, in a single, sad, linear thread. If you have 500 pages and each takes 100ms, that’s 50 seconds. Yawn. Hugo looks at that pile and says, “I’ve got 8 CPU cores and a need for speed,” and it fans that work out across hundreds or even thousands of goroutines.

A goroutine is Go’s version of a super lightweight thread. Spinning one up costs almost nothing in terms of memory, and the Go scheduler efficiently maps them onto your actual OS threads. This allows Hugo to have a separate goroutine for each page it needs to render. It’s an “embarrassingly parallel” problem, and Hugo is not embarrassed to parallelize the heck out of it.

The Nuts and Bolts of Page Processing

Hugo’s rendering pipeline is a well-orchestrated concurrent system. It doesn’t just throw all your pages into a giant pool and hope for the best. Here’s a simplified view of the lifecycle of a single page render inside a goroutine:

  1. Page Creation: Hugo first parses your content and configuration to create an in-memory object representing the page, its data, and its templates.
  2. Dependency Resolution: This is the clever part. The page goroutine figures out what this page needs: its specific template, any partials, the data from your data/ files, etc. Many of these dependencies are already loaded into memory.
  3. Template Execution: The goroutine executes the resolved templates, passing in the page object. This is where the HTML is actually generated.
  4. Writing to Disk: Finally, the goroutine writes the finished HTML to a file in the public/ directory.

The beauty is that these four steps are happening for dozens of pages simultaneously. Your homepage, your blog post from 2015, and that weird taxonomy list page are all being processed at the exact same time.

Where This Model Breaks Down (The Pitfalls)

It’s not all rainbows and unicorns. Concurrency introduces complexity, and you will run into issues if you’re not careful. The main villain is shared state.

Let’s say you use a custom function in your template that relies on a global counter or writes to a shared map. This is a recipe for race conditions. In a linear build, it might work by accident. Under parallel rendering, your counter will be utterly nonsensical and your build will be unpredictable.

// layouts/partials/bad-idea.html
// This will cause non-deterministic, garbage output.
{{ $globalCounter := .Scratch.Get "myCounter" }}
{{ .Scratch.Set "myCounter" (add $globalCounter 1) }}
<p>This is page number: {{ $globalCounter }}</p>

The .Scratch is per-page, for this exact reason. If you need to share data across the entire site, it must be done at build-time initialization, not during page rendering. Use your site’s config or data files, and pre-calculate what you need before the parallel rendering frenzy begins.

Another common pitfall is assuming filesystem operations happen in order. If two pages write to the same file, you’ve got a race. Hugo’s core doesn’t do this, but it’s a warning if you start writing custom hooks or functions.

Taming the Chaos: Best Practices

To play nicely with Hugo’s parallel engine, you need to think functionally. Your templates and rendering logic should be idempotent and stateless. Given the same input (page data), they should always produce the same output, without side effects on other parts of the system.

  1. Leverage Scratch: The .Scratch attached to each page is your best friend for storing page-specific state. It’s a safe, isolated sandbox for that page’s goroutine.
  2. Pre-compute in Data Files: If you need a “total post count” displayed on every page, don’t calculate it during each page render. Calculate it once in your data/ processing or site config and then reference that pre-computed value.
  3. Be Mindful of Heavy Logic: A hugely expensive partial that gets called on every page can spawn hundreds of expensive operations simultaneously, potentially exhausting memory. Cache what you can, and be efficient. Hugo is fast, but it’s not a supercomputer.

You can actually see the parallel magic in action by running Hugo with the --verbose flag. The log output will be a chaotic, interleaved mess of messages from different pages. It’s beautiful. It means it’s working.

So, the next time you run a build and it finishes before you’ve even lifted your finger from the Enter key, thank the goroutine—the tiny, mighty worker that makes it all possible.