---
title: Hot reload and file watching
description: "Why Pennington ships its own file watcher and WebSocket reload channel, and how the dev-only script is kept out of published builds."
canonical_url: https://usepennington.net/explanation/dev-experience/hot-reload/
sidecar_url: https://usepennington.net/explanation/dev-experience/hot-reload.md
content_hash: sha256:fae8853e4b15083ef27d125f9f72dffa521582abc5ec9643937651aba0bb6a08
tokens: 1706
uid: explanation.dev-experience.hot-reload
reading_time_minutes: 5
---

Under the Hood
# Hot reload and file watching

Why Pennington ships its own file watcher and WebSocket reload channel, and how the dev-only script is kept out of published builds.

 
Content files — `.md` sources, front matter, images, assets tracked under a source's `ContentPath` — are not part of the .NET compilation. Restarting the host for every markdown typo would tear down Kestrel and throw away the expensive in-memory caches that make the second request fast. Pennington instead watches content in process and reloads only the browser: an in-process file watcher, services that discard and rebuild their derived caches on change, and a debounced WebSocket channel through which the browser is told to reload. WebSocket also makes server restarts easy to detect on the client — the socket closes and reconnects, and the browser reloads — without the careful `onerror` plumbing SSE would need for the same signal. Polling, the third alternative, adds latency under load and noise under idle.

 
## How it works

 
The mechanism is a single chain: files change, cached services drop their state, a debounce window elapses, and the browser reloads.

 
### Watching content directories

 
A service tells the engine which directories to watch by declaring `WatchScopes` — the public contract through which every file-reactive service registers its content roots. Creates, deletes, and renames count as changes, not just edits in place, so a new markdown file or a deleted asset reloads the same way a save does. The watcher behaves consistently across Windows and WSL, the two platforms most contributors run, so the dev loop feels the same on either.

 
### The `IFileWatchAware` contract

 
Several services build expensive lookup tables from disk on startup: link resolvers, cross-reference uid maps, search indexes, sitemaps, and blog content resolvers. They register through `AddFileWatched<T>`, which is constrained to types implementing `IFileWatchAware` — the single contract every file-reactive service shares. A service declares the directories it needs watched and an `OnFileChanged` method whose return value says how the change should be handled: `Ignore` it, report it `Refreshed` its own state, or ask to be `Recreate`d.

 
The services above return `Recreate`, so on the next request the stale instance is dropped — disposed if it implements `IDisposable` — and a fresh one is rebuilt through normal constructor injection. The approach is to discard and rebuild the whole instance rather than bust individual caches: no service needs to know when to flush itself, because the engine replaces the entire instance when the underlying content moves.

 
This is the extension point you reach for when your own service caches something derived from content files. The how-tos that lean on it: [write a custom content service](https://usepennington.net/how-to/content-services/custom-content-service.md), [publish a custom feed from a content service](https://usepennington.net/how-to/feeds/custom-feed.md), and [use a YAML or JSON data file in pages](https://usepennington.net/how-to/content-services/data-files.md).

 
### Debouncing and broadcasting over WebSocket

 
`LiveReloadServer` resets a 300ms debounce timer on every change notification, so it broadcasts a single reload only after 300ms of quiet — coalescing rapid saves (editor auto-save, multi-file renames) into one browser reload. It listens on the WebSocket endpoint `/__pennington/reload`, which is worth knowing if a reverse proxy or a Content-Security-Policy sits in front of the dev server and needs to allow the upgrade.

 
### Script injection and reconnection

 
`LiveReloadScriptProcessor` is an `IResponseProcessor` at `Order = 20`, positioned between the HTML rewriting pipeline at `Order = 10` and the diagnostic overlay at `Order = 30`. When active it finds the last `</body>` tag and inserts an inline script that opens a WebSocket to `/__pennington/reload`. The script includes three refinements over a naive `location.reload()` approach: a `beforeunload` guard that suppresses reconnect attempts during normal page navigation, a 150ms delay before reload so the response pipeline has time to settle, and a reload on reconnect so that a host restart refreshes the browser without waiting for a file-change message.

 
### Build-mode gating

 
Both `LiveReloadScriptProcessor` and `UseLiveReload` check `PenningtonCli.Current.IsHeadlessOneShot` — true for any headless one-shot run, which covers `build` and `diag` alike, not just the `build` verb. When it is true, the processor's `ShouldProcess` returns `false` and the middleware skips endpoint registration entirely. This means the `OutputGenerationService` crawler sees clean HTML with no script and no WebSocket endpoint: no publish-time stripping step, no build configuration to set, and no dev-only flag to forget.

 
## Relation to `dotnet watch` and .NET Hot Reload

 
This is a different layer from .NET Hot Reload and `dotnet watch`, and the two are complementary. .NET Hot Reload patches running CLR code — your `.cs` and `.razor` source — and `dotnet watch` restarts or re-applies edits when compiled code changes. Pennington's watcher covers the half they don't: content files (`.md`, front matter, images, `_meta.yml`, data files) that never enter the compilation. Run the host under `dotnet watch` and you get both — code edits patched by the runtime, content edits reloaded by Pennington — without either stepping on the other.

 
## Disabling reload or tuning the debounce

 
In serve mode live reload is always on, and the 300ms debounce and reconnection behavior are fixed; there is no option to tune them or switch reload off while still serving. The single off-switch is build mode: a headless one-shot run (`build` or `diag`) gates the whole subsystem out, which is exactly what you want for published output. If you need a dev server with no reload at all, host the engine without `UsePennington`'s dev path rather than reaching for a flag.

 
## Further reading

 
 - Reference: [DI and middleware extension methods](https://usepennington.net/reference/host/extensions.md)
 - Reference: [Response processing interfaces](https://usepennington.net/reference/api/i-response-processor.md)
 - Explanation: [The response-processing pipeline](https://usepennington.net/explanation/core/response-processing.md)
 - Explanation: [Dev mode and build mode share one code path](https://usepennington.net/explanation/core/dev-vs-build.md)
 
 
[Previous
                
                Locale-aware URLs and content fallback](https://usepennington.net/explanation/localization/urls-and-fallback.md)[Next
                    
                What the DocSite and BlogSite templates wire for you](https://usepennington.net/explanation/positioning/docsite-positioning.md)