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
- Retry the metadata request after a short delay; the lock is usually released once the update completes.
- Avoid issuing metadata reads concurrently with publish/update calls for the same database.
- 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
- Don't interleave metadata reads with publishes for the same database
- Apply exponential backoff on lock-related failures
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
- database program changed before publication
- environment-only publication requires…
- found is_anonymous= in st_view, but in module when updating…
- repo
- repo : error getting file metadata for segment
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)