juanfont/headscale · critical

dropping routes table: %w

Error message

dropping routes table: %w

What it means

During the v0.27.0 SQLite-only schema recreation migration, DROP TABLE routes failed. The migration drops a leftover routes table (normally already removed by migration 202502131714) before renaming and rebuilding all core tables. Failure means SQLite rejected the DROP: database locked by another connection, read-only file, or insufficient permissions.

Source

Thrown at hscontrol/db/db.go:269

						log.Info().Msg("skipping schema migration on non-SQLite database")
						return nil
					}

					log.Info().Msg("starting schema recreation with table renaming")

					// Rename existing tables to _old versions
					tablesToRename := []string{"users", "pre_auth_keys", "api_keys", "nodes", "policies"}

					// Check if routes table exists and drop it (should have been migrated already)
					var routesExists bool

					err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'").Row().Scan(&routesExists)
					if err == nil && routesExists {
						log.Info().Msg("dropping leftover routes table")

						err := tx.Exec("DROP TABLE routes").Error
						if err != nil {
							return fmt.Errorf("dropping routes table: %w", err)
						}
					}

					// Drop all indexes first to avoid conflicts
					indexesToDrop := []string{
						"idx_users_deleted_at",
						"idx_provider_identifier",
						"idx_name_provider_identifier",
						"idx_name_no_provider_identifier",
						"idx_api_keys_prefix",
						"idx_policies_deleted_at",
					}

					for _, index := range indexesToDrop {
						_ = tx.Exec("DROP INDEX IF EXISTS " + index).Error
					}

					for _, table := range tablesToRename {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Ensure no other process has the SQLite file open: stop old containers/instances and CLI commands.
  2. Verify write permission on both the database file and its directory.
  3. Retry startup - the existence check and DROP are idempotent.
  4. If it persists, open the DB with the sqlite3 shell and DROP TABLE routes manually (data was already migrated), then restart.
Defensive patterns

Strategy: try-catch

Try / catch

if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
    if strings.Contains(err.Error(), "dropping routes table") {
        // SQLite lock or read-only file: stop other sqlite users, fix perms, restart
    }
}

Prevention

When it happens

Trigger: Another process (old headscale instance, CLI, sqlite3 shell) holds the database while startup migration runs; SQLite file or directory not writable (journal creation fails).

Common situations: Restarting headscale while the previous instance has not fully exited; database on NFS without proper locking; container running with a read-only mounted volume.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/ddd9e12526cc31b1. Report an issue: GitHub.