juanfont/headscale · error
busy_timeout must be >= 0
Error message
busy_timeout must be >= 0
What it means
Exported validation error from hscontrol/db/sqliteconfig rejecting a negative busy_timeout in the SQLite DSN. busy_timeout (ms) controls how long SQLite waits on a locked database before returning SQLITE_BUSY; negative values are meaningless and are rejected by Validate(). The default is DefaultBusyTimeout = 10000 ms.
Source
Thrown at hscontrol/db/sqliteconfig/config.go:15
// Package sqliteconfig provides type-safe configuration for SQLite databases
// with proper enum validation and URL generation for modernc.org/sqlite driver.
package sqliteconfig
import (
"errors"
"fmt"
"slices"
"strings"
)
// Errors returned by config validation.
var (
ErrPathEmpty = errors.New("path cannot be empty")
ErrBusyTimeoutNegative = errors.New("busy_timeout must be >= 0")
ErrInvalidJournalMode = errors.New("invalid journal_mode")
ErrInvalidAutoVacuum = errors.New("invalid auto_vacuum")
ErrWALAutocheckpoint = errors.New("wal_autocheckpoint must be >= -1")
ErrInvalidSynchronous = errors.New("invalid synchronous")
ErrInvalidTxLock = errors.New("invalid txlock")
)
const (
// DefaultBusyTimeout is the default busy timeout in milliseconds.
DefaultBusyTimeout = 10000
)
// JournalMode represents SQLite journal_mode pragma values.
// Journal modes control how SQLite handles write transactions and crash recovery.
//
// Performance vs Durability Tradeoffs:
//
// WAL (Write-Ahead Logging) - Recommended for production:View on GitHub (pinned to 565fd254d0)
Solutions
- Set busy_timeout to a non-negative value — 0 disables waiting, the default 10000 (ms) is a good baseline
- Increase it (e.g. 10000-30000) rather than making it negative if you are fighting 'database is locked' errors
- Audit env overrides like HEADSCALE_DB_SQLITE_BUSY_TIMEOUT for parsing bugs
Example fix
# before
database:
sqlite:
busy_timeout: -1
# after
database:
sqlite:
busy_timeout: 10000 Defensive patterns
Strategy: validation
Validate before calling
if cfg.BusyTimeout != nil && *cfg.BusyTimeout < 0 {
return fmt.Errorf("invalid busy_timeout %d: %w", *cfg.BusyTimeout, sqliteconfig.ErrBusyTimeoutNegative)
} Type guard
func validBusyTimeout(ms *int) bool { return ms == nil || *ms >= 0 } Try / catch
if err := cfg.Validate(); err != nil {
if errors.Is(err, sqliteconfig.ErrBusyTimeoutNegative) {
return errors.New("busy_timeout must be >= 0 (0 disables waiting; default 10000 ms)")
}
return err
} Prevention
- Parse numeric config with strict validators; reject sign errors at load time
- Use 0 to disable busy-waiting, never a negative sentinel
- For 'database is locked' errors raise busy_timeout instead of mutating it blindly
When it happens
Trigger: Setting database.sqlite.busy_timeout to a negative number in config, or passing a negative BusyTimeout when constructing the config programmatically, then generating the DSN.
Common situations: Env var override parsed with a stray '-' sign; config templating arithmetic producing -1 as a sentinel; misunderstanding 0 (disable) vs negative.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- path cannot be empty
- wildcard (*) is not supported as SSH destination
- is above the max (168h)
- oidc.client_id is required when oidc.issuer is set
- oidc.client_secret or oidc.client_secret_path is required wh
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/0b374bf627f8b782.
Report an issue: GitHub.