Skip to content

Watch mode

fastgrpc dev runs a continuous build-and-serve loop powered by watchfiles, which wraps the Rust notify crate over OS-native file events (FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows).

The reload pipeline

edit main.py / business.py / models.py
        ↓ (Rust watcher fires)
purge user modules from sys.modules
re-import main.py — decorators re-register services
inspector → lock → validator → proto writer → protoc compile
stop the running server, start a new one with rewired handlers

Behavior worth knowing

Transitive imports reload

Editing business.py or models.py triggers a full rebuild, not just main.py. Without an explicit sys.modules purge, Python would serve cached versions of nested modules and your changes would silently disappear.

The purge is conservative: only modules whose __file__ lives under the entry-file's parent directory are evicted.

Framework modules persist

fastgrpc, grpc, and google.protobuf are excluded from the purge. Re-importing megabytes of generated grpc stubs on every keystroke would be unusably slow, and those modules don't contain user code that needs to reload.

Only .py files trigger reload

Generated .proto and _pb2.py artifacts are filtered out by the watcher. Without this filter, writing the regenerated proto would itself trigger another reload — an infinite loop.

Reload errors are isolated

If the new build fails (syntax error, validation failure, lock conflict, etc.), the previous server keeps serving traffic and the error is printed to stdout. You fix the file, save it again, and the next reload attempt either succeeds or prints another error. Your terminal is the only thing that breaks.

Why a Rust watcher

A pure-Python watcher would have to os.stat() every file every second. That's expensive on large repos and slow to react. The Rust notify crate hooks into kernel-level event APIs, so:

  • No polling — Python wakes only when the OS reports a change
  • Sub-millisecond reaction time
  • Scales to repos with tens of thousands of files

The Python wrapper is exposed as a regular iterator via PyO3 bindings; from your side it's just for changes in watch(...).

Disabling watch

For production, use fastgrpc run instead of fastgrpc dev. It runs the same codegen pipeline once at startup but doesn't start a watcher and doesn't enable reflection.