juanfont/headscale · error
saving API key to database: %w
Error message
saving API key to database: %w
What it means
Returned by HSDatabase.CreateAPIKey when GORM's DB.Save(&key) fails while persisting a newly generated API key. The secret has already been bcrypt-hashed, so the error is purely a database write failure: connection loss, constraint violation, SQLite database lock, or insufficient disk/permissions. The generated key string is discarded (empty string returned) because the caller never receives it.
Source
Thrown at hscontrol/db/api_key.go:57
secret := rands.HexString(apiKeyHashLength)
// Full key string (shown ONCE to user)
keyStr := apiKeyPrefix + prefix + "-" + secret
// bcrypt hash of secret
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
return "", nil, err
}
key := types.APIKey{
Prefix: prefix,
Hash: hash,
Expiration: expiration,
}
if err := hsdb.DB.Save(&key).Error; err != nil { //nolint:noinlineerr
return "", nil, fmt.Errorf("saving API key to database: %w", err)
}
return keyStr, &key, nil
}
// ListAPIKeys returns the list of [types.APIKey] values for a user.
func (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) {
keys := []types.APIKey{}
err := hsdb.DB.Find(&keys).Error
if err != nil {
return nil, err
}
return keys, nil
}
// GetAPIKey returns a [types.APIKey] for a given key.View on GitHub (pinned to 565fd254d0)
Solutions
- Check the wrapped error text: 'database is locked' means concurrent SQLite access - stop other writers or move to PostgreSQL.
- Verify database connectivity and disk space on the database host.
- Confirm the api_keys schema matches the binary version (run migrations / use the same headscale version for CLI and server).
- Retry the API key creation; prefixes are random so a unique-index collision is effectively impossible on retry.
Example fix
// before
keyStr, key, err := hsdb.CreateAPIKey(userID, expiration, nil)
if err != nil {
log.Fatal().Err(err).Msg("failed")
}
// after
keyStr, key, err := hsdb.CreateAPIKey(userID, expiration, nil)
if err != nil {
if strings.Contains(err.Error(), "database is locked") {
// retry once after the SQLite writer finishes, or route writes through one process
}
return fmt.Errorf("creating API key: %w", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Verify the database is writable before generating a key
var one int
if err := hsdb.DB.Raw("SELECT 1").Scan(&one).Error; err != nil {
return fmt.Errorf("database not ready: %w", err)
} Type guard
func isTransientDBError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "database is locked") ||
strings.Contains(msg, "SQLSTATE 40001") ||
strings.Contains(msg, "connection refused")
} Try / catch
keyStr, key, err := hsdb.CreateAPIKey(userID, exp, nil)
if err != nil {
if isTransientDBError(err) {
// single retry; prefixes are random so no collision risk
keyStr, key, err = hsdb.CreateAPIKey(userID, exp, nil)
}
if err != nil {
return fmt.Errorf("creating API key: %w", err)
}
} Prevention
- Run only one headscale process (server or CLI) against a SQLite database at a time.
- Prefer PostgreSQL for deployments where the CLI and server run concurrently.
- Monitor disk space on the database host; bcrypt hashes plus rows are small but the failure mode is binary.
When it happens
Trigger: Calling CreateAPIKey (e.g. via 'headscale apikeys create') while the database is locked by another writer (SQLite 'database is locked'), the DB connection has dropped, the api_keys table schema is out of sync with types.APIKey, or the unique index idx_api_keys_prefix collides with an existing prefix.
Common situations: SQLite deployments where the headscale CLI and server run concurrently and contend for the file; Postgres unreachable mid-operation; disk full on the host; a database restored from an older headscale version whose api_keys table lacks columns GORM tries to write.
Related errors
- automigrating types.Route: %w
- automigrating types.Node: %w
- adding prefix column: %w
- adding hash column: %w
- getting DB from gorm: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/7764c01b139355d6.
Report an issue: GitHub.