How Hugo's Live Reload Server Detects and Pushes File Changes
Hugo's live reload server detects file changes using fsnotify (or a polling fallback), triggers a site rebuild via the hugoBuilder, and pushes refresh commands to browsers through a WebSocket hub that broadcasts to all connected clients.
Hugo's live reload feature is a self-contained pipeline that eliminates manual browser refreshing during development. Implemented in the gohugoio/hugo repository, this system combines filesystem monitoring with WebSocket communication to provide instant feedback when content or assets change.
The Three-Component Architecture
The live reload pipeline consists of three coordinated components: a filesystem watcher, a build trigger, and a WebSocket broadcast hub.
File System Watcher
Hugo monitors directories using fsnotify for native OS events, with a polling-based fallback for platforms lacking native support. The watcher implementation resides in watcher/batcher.go and watcher/filenotify/filenotify.go.
When hugo server starts with --watch (the default), the command builds a directory list and initializes the watcher:
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
The newWatcher factory (in commands/hugobuilder.go) returns either an fsnotify.Watcher or a PollingWatcher, both implementing the FileWatcher interface. Events are batched and delivered as Event structs containing the file path and operation type.
Build Trigger and Refresh Path
When the watcher detects a change, it invokes hugoBuilder.Build() to regenerate the site. Upon successful completion, the system calls livereload.RefreshPath with the changed file's path.
In watcher/batcher.go, the event loop triggers the rebuild:
go func() {
for ev := range w.Events {
if err := b.builder.Build(); err == nil {
livereload.RefreshPath(ev.Name)
}
}
}()
The RefreshPath function (in livereload/livereload.go) constructs a JSON command and sends it to the WebSocket hub:
msg := fmt.Sprintf(`{"command":"reload","path":%q,"originalPath":"","liveCSS":true,"liveImg":true}`, urlPath)
wsHub.broadcast <- []byte(msg)
WebSocket Hub and Client Notification
The hub (livereload/hub.go) maintains a registry of active WebSocket connections and broadcasts messages to all clients. It runs a dedicated goroutine (hub.run) that handles connection registration, unregistration, and message distribution.
The WebSocket handler in livereload/livereload.go upgrades HTTP requests and registers new connections:
upgrader.Upgrade(w, r, nil)
// Connection registered with wsHub
The client-side component consists of livereload.js, embedded in the binary using //go:embed livereload.min.js and served at /livereload.js. This script opens a WebSocket to ws://localhost:1313/livereload, listens for "reload" commands, and either refreshes the entire page or hot-swaps CSS/images without a full reload.
Disabling Live Reload
The --disableLiveReload flag prevents the WebSocket hub initialization. When enabled, the server skips livereload.Initialize(), and calls to RefreshPath become no-ops. The filesystem watcher continues to rebuild the site, but browsers no longer receive automatic refresh commands.
hugo server --disableLiveReload
Summary
- File watching uses
fsnotifywith a polling fallback, implemented inwatcher/batcher.goandwatcher/filenotify/filenotify.go. - Build triggering occurs in
commands/hugobuilder.go, where the watcher callback invokeshugoBuilder.Build()followed bylivereload.RefreshPath. - WebSocket broadcasting happens through the hub in
livereload/hub.go, which pushes JSON reload commands to all connected browsers. - Client-side handling is managed by the embedded
livereload.jsscript, which performs full page reloads or asset hot-swapping based on server messages.
Frequently Asked Questions
How does Hugo detect file changes on systems without native file system events?
Hugo falls back to a polling watcher implemented in watcher/filenotify/poller.go. This watcher periodically scans the configured directories and compares file states to detect modifications. While this consumes more CPU than native fsnotify events, it ensures compatibility with network drives, Docker containers, and operating systems with limited file system event support.
What message format does Hugo use to trigger browser refreshes?
The server sends a JSON WebSocket message with the following structure:
{"command":"reload","path":"/css/style.css","originalPath":"","liveCSS":true,"liveImg":true}
The path field indicates the changed asset, while liveCSS and liveImg boolean flags tell the client-side livereload.js whether it can perform a hot replacement of stylesheets or images without reloading the entire page.
Can I use Hugo's live reload system with external build tools or custom pipelines?
Yes, the livereload package is programmatically accessible. External tools can import github.com/gohugoio/hugo/livereload, call livereload.Initialize() to start the WebSocket server, and invoke livereload.RefreshPath("path/to/file") after their own build steps complete. This allows integration with custom asset pipelines, external CMS updates, or hybrid development environments where Hugo runs alongside other build tools.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →