# How Hugo's Live Reload Server Detects and Pushes File Changes

> Discover how Hugo's live reload server detects file changes using fsnotify, triggers rebuilds, and pushes updates to browsers via WebSockets for a seamless development experience.

- Repository: [GoHugo.io/hugo](https://github.com/gohugoio/hugo)
- Tags: internals
- Published: 2026-02-28

---

**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`](https://github.com/gohugoio/hugo/blob/main/watcher/batcher.go) and [`watcher/filenotify/filenotify.go`](https://github.com/gohugoio/hugo/blob/main/watcher/filenotify/filenotify.go).

When `hugo server` starts with `--watch` (the default), the command builds a directory list and initializes the watcher:

```go
watcher, err := c.newWatcher(c.r.poll, watchDirs...)

```

The `newWatcher` factory (in [`commands/hugobuilder.go`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/watcher/batcher.go), the event loop triggers the rebuild:

```go
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`](https://github.com/gohugoio/hugo/blob/main/livereload/livereload.go)) constructs a JSON command and sends it to the WebSocket hub:

```go
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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/livereload/livereload.go) upgrades HTTP requests and registers new connections:

```go
upgrader.Upgrade(w, r, nil)
// Connection registered with wsHub

```

The client-side component consists of [`livereload.js`](https://github.com/gohugoio/hugo/blob/main/livereload.js), embedded in the binary using `//go:embed livereload.min.js` and served at [`/livereload.js`](https://github.com/gohugoio/hugo/blob/main//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.

```bash
hugo server --disableLiveReload

```

## Summary

- **File watching** uses `fsnotify` with a polling fallback, implemented in [`watcher/batcher.go`](https://github.com/gohugoio/hugo/blob/main/watcher/batcher.go) and [`watcher/filenotify/filenotify.go`](https://github.com/gohugoio/hugo/blob/main/watcher/filenotify/filenotify.go).
- **Build triggering** occurs in [`commands/hugobuilder.go`](https://github.com/gohugoio/hugo/blob/main/commands/hugobuilder.go), where the watcher callback invokes `hugoBuilder.Build()` followed by `livereload.RefreshPath`.
- **WebSocket broadcasting** happens through the hub in [`livereload/hub.go`](https://github.com/gohugoio/hugo/blob/main/livereload/hub.go), which pushes JSON reload commands to all connected browsers.
- **Client-side handling** is managed by the embedded [`livereload.js`](https://github.com/gohugoio/hugo/blob/main/livereload.js) script, 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`](https://github.com/gohugoio/hugo/blob/main/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:

```json
{"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`](https://github.com/gohugoio/hugo/blob/main/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.