clockworklabs/SpacetimeDB · error

unable to lock database for environment metadata

Error message

unable to lock database for environment metadata

What it means

This error is thrown when a read lock on the module host for a replica cannot be acquired while fetching environment metadata. The controller uses a tokio RwLock per replica; if a write lock (e.g. a module update) is held for too long or the lock is poisoned/closed, acquire_read_lock fails and the caller gets this generic anyhow error instead of metadata. It is a transient concurrency failure, not data corruption.

Solutions

  1. Retry the metadata request after a short delay; the lock is usually released once the update completes.
  2. Avoid issuing metadata reads concurrently with publish/update calls for the same database.
  3. If it persists, check server logs for a stuck module update holding the replica write lock and restart the host process if needed.

Example fix

// before: fire-and-forget concurrent calls
metadata = await fetchMetadata(db);
await publish(db, bytes);

// after: sequence operations or retry with backoff
await publish(db, bytes);
let metadata = await retry(() => fetchMetadata(db), { retries: 3, backoffMs: 250 });
Defensive patterns

Strategy: retry

Validate before calling

// no pre-check available; treat as transient
if (err.message.includes('unable to lock database for environment metadata')) scheduleRetry();

Try / catch

try { meta = await fetchMetadata(db); } catch (e) { if (/unable to lock database/.test(e.message)) await sleep(250).then(fetchMetadata); else throw e; }

Prevention

When it happens

Trigger: Calling the environment-metadata API (which returns spacetimedb_client_api_messages::publish::EnvironmentMetadata) while the replica's module host is locked by another operation, e.g. an in-flight publish/update_database of the same database, or during shutdown when the lock is closed.

Common situations: A developer queries database metadata via HTTP API at the same time another client republishes the module; CI scripts racing a `spacetime publish` with metadata reads; querying during server shutdown.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-09-20). Data as JSON: /api/errors/cd0ceca589fc499e. Report an issue: GitHub.

Appendix: source

Thrown at crates/core/src/host/host_controller.rs:818

        let guard = self.acquire_read_lock(replica_id).await.map_err(|_| {
            warn!("timeout waiting for read lock on replica {replica_id} in `get_module_host`");
            NoSuchModule
        })?;
        guard
            .as_ref()
            .map(|Host { module, .. }| module.borrow().clone())
            .ok_or(NoSuchModule)
    }

    /// Read environment metadata while preventing module replacement or shutdown.
    pub async fn environment_metadata(
        &self,
        replica_id: u64,
    ) -> anyhow::Result<spacetimedb_client_api_messages::publish::EnvironmentMetadata> {
        let guard = self
            .acquire_read_lock(replica_id)
            .await
            .map_err(|_| anyhow::anyhow!("unable to lock database for environment metadata"))?;
        let module = guard.as_ref().ok_or(NoSuchModule)?.module.borrow().clone();
        let stored_keys = module.relational_db().with_read_only(Workload::Internal, |tx| {
            crate::db::environment::snapshot(tx).map(|values| values.into_keys().collect())
        })?;
        Ok(spacetimedb_client_api_messages::publish::EnvironmentMetadata {
            module_version: module.info.module_hash.to_string(),
            declarations: module.info.module_def.environment().declarations().cloned().collect(),
            stored_keys,
        })
    }

    /// Subscribe to updates of the [`ModuleHost`] identified by `replica_id`,
    /// or return an error if it is not registered with the controller.
    ///
    /// See [`Self::watch_maybe_launch_module_host`] for a variant which
    /// launches the host if it is not running.
    #[tracing::instrument(level = "trace", skip_all)]
    pub async fn watch_module_host(&self, replica_id: u64) -> Result<watch::Receiver<ModuleHost>, NoSuchModule> {

View on GitHub (pinned to eddf9f5014)