Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Air for Go: Live Reload That Does Not Rebuild on Every Saved File

Sean

Platform Writer

Aug 13, 2026
7 min read

Air is a live-reloading runner for Go: it watches your files, runs go build, and restarts the binary when something changes. Install it, run air, and the compile-restart cycle disappears. The configuration that matters is the exclusion list — the default watches a lot, so an unconfigured Air rebuilds when you save a README, a test file, or something your editor wrote to tmp/.

Air for Go: Live Reload That Does Not Rebuild on Every Saved File

Go compiles fast enough that people ask whether this is worth it. On a small service the answer is marginal; once the build takes three seconds and you are iterating on a handler, it is the difference between staying in flow and not.

Table of contents

Install and run

# Install the binary
go install github.com/air-verse/air@latest

# Make sure it is on PATH
export PATH="$PATH:$(go env GOPATH)/bin"

# Generate a config in the project root
air init

# Run
air

Note the module path: the project moved from cosmtrek/air to air-verse/air. Older tutorials and Dockerfiles still reference the old path, which now redirects but is worth updating.

air init writes .air.toml with defaults. Running bare air without a config uses built-in defaults, which work but watch more than you want.

A config worth using

root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main ./cmd/server"
  bin = "./tmp/main"
  full_bin = "APP_ENV=dev ./tmp/main"

  include_ext = ["go", "tpl", "tmpl", "html", "sql"]
  exclude_dir = ["assets", "tmp", "vendor", "node_modules", ".git", "testdata"]
  exclude_regex = ["_test\\.go", "_gen\\.go"]
  exclude_unchanged = true
  follow_symlink = false

  delay = 300
  stop_on_error = true
  send_interrupt = true
  kill_delay = 2000

[log]
  time = true

[misc]
  clean_on_exit = true

The settings that earn their place:

  • include_ext — only these extensions trigger a rebuild. Without it, saving a markdown file restarts your server.
  • exclude_regex with _test\.go — test files do not affect the running binary, so rebuilding on them is pure waste.
  • delay — debounce in milliseconds. Editors that save several files at once otherwise trigger several builds.
  • send_interrupt and kill_delay — send SIGINT and wait, so your graceful shutdown runs. Without this Air kills the process outright and you never exercise the shutdown path you wrote.
  • stop_on_error — keep the last working binary running when a build fails, rather than leaving nothing serving.

send_interrupt is the one worth calling out. If your server has a Shutdown handler on SIGTERM, this is what lets you see it working during development instead of discovering in production that it never ran.

Excluding what your editor and tooling generate

The most common complaint about Air is a rebuild loop, and it is almost always something writing into a watched directory.

  • tmp/ must be excluded — Air writes the binary there, and watching it means the build triggers a rebuild.
  • Code generators writing .go files into the tree cause a loop unless the output matches exclude_regex.
  • vendor/ is large and static; watching it wastes file descriptors.
  • Editor swap and backup files — .swp, .tmp, #file# — should be excluded by extension.
  • SQLite databases and log files written by the app itself, if they live in the project directory.

If Air rebuilds continuously with no input from you, run it and watch which path it reports as changed. The message names the file, and the file is always something being written by a process you forgot about.

Air in Docker Compose

Useful when the service needs a database beside it, so the whole development environment comes up with one command.

# Dockerfile.dev
FROM golang:1.24-alpine
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["air", "-c", ".air.toml"]
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - go-mod-cache:/go/pkg/mod
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://dev:dev@db:5432/dev?sslmode=disable
    depends_on:
      - db

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: dev
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  go-mod-cache:
  pgdata:

The go-mod-cache volume is what stops every container restart re-downloading your dependencies. Without it the developer experience is worse than not using Docker at all.

One platform note: file change events do not always propagate reliably through bind mounts on macOS and Windows. If Air sees no changes inside the container, add poll = true to the [build] section — it is less efficient but it works everywhere.

The alternatives, briefly

  • wgo — smaller and simpler, no config file, good when you want wgo run ./cmd/server and nothing else.
  • reflex — general-purpose file watcher, not Go-specific, more flexible and more configuration.
  • entr — Unix tool, composes well: ls **/*.go | entr -r go run ./cmd/server.
  • Nothing — for a small service, go run ./cmd/server and a keystroke is honestly fine.

Air is the most featured and the most widely used. entr is the most Unix-shaped and needs no Go-specific tooling at all. Pick based on whether you want configuration or composition.

Development reload is not deployment

Worth stating plainly because the phrase “hot reload” invites the confusion: Air is a development tool. It rebuilds on your machine, from your working tree, with whatever uncommitted changes you have.

Production does not rebuild in place. It builds a specific commit in a clean environment, produces an artefact, and starts that. The reason is repeatability — a binary built from your working tree is not reproducible by anyone else, and the second it fails you cannot tell whether the code or the environment is responsible.

Go makes the production side easy, since go build yields one static binary and the runtime image can be a few megabytes. Building from a connected GitHub repository — with a build log for the compile, a live route, environment variables for the database URL, and the previous deploy still there to roll back to — is the shape RunxBuild handles for a Go service, and it is deliberately not the same mechanism as the one watching your files.

How this fits the rest of the stack

Install from github.com/air-verse/air, run air init, then spend five minutes on the config. Set include_ext so only real source changes rebuild, exclude tmp/ and vendor/ and test files, add a delay to debounce multi-file saves, and turn on send_interrupt so your graceful shutdown actually runs in development.

If it rebuilds in a loop, something is writing into a watched directory — Air names the file. And keep it clearly separate from deployment, which should build a known commit rather than your working tree. If you are pricing that out, the RunxBuild hosting calculator itemises service, database, storage, and bandwidth.

Useful related references:

FAQ

How do I install Air for Go?

go install github.com/air-verse/air@latest, and make sure $(go env GOPATH)/bin is on your PATH. Note the module moved from cosmtrek/air to air-verse/air; older tutorials reference the previous path, which still redirects but is worth updating.

Why does Air rebuild continuously?

Something is writing into a watched directory. The usual culprits are the tmp/ directory where Air puts the binary, a code generator producing .go files, or editor swap files. Air prints the path that changed, which identifies it immediately. Add the directory to exclude_dir or the pattern to exclude_regex.

How do I stop Air rebuilding on test files?

Add _test\.go to exclude_regex in the [build] section of .air.toml. Test files do not affect the running binary, so rebuilding on them is wasted time. Setting include_ext to just the extensions that matter also prevents markdown and config saves triggering builds.

Does Air work inside Docker?

Yes — install it in a development Dockerfile and bind-mount your source. Add a named volume for /go/pkg/mod so dependencies are not re-downloaded on every restart. If file change events do not propagate through the bind mount on macOS or Windows, set poll = true in the build section.

Should I use Air in production?

No. Air rebuilds from your working tree, which is not reproducible by anyone else. Production should build a specific commit in a clean environment and run that artefact, so a failure is attributable and the previous version is still available to roll back to.

#golang#air#live reload#hot reload#go development