libnyanpasu/clash-nyanpasu · error
write config timed out after {timeout:?}
Error message
write config timed out after {timeout:?} What it means
This error is produced by `PersistentStateManager::upsert` when the config-persist effect that follows a successful state commit does not finish within the allotted timeout. The state change itself committed (WithEffectError::EffectTimedOut), but the write-to-disk side effect was still running when the deadline expired, so the operation is reported as UpsertError::WriteConfig. It signals degraded durability: the in-memory state is updated but the caller cannot confirm the config file was written.
Source
Thrown at backend/nyanpasu-core/src/state/manager/persistent_state.rs:214
let config_prefix = self.config_prefix.clone();
let formatter = self.formatter.clone();
self.state_coordinator
.with_pending_state(&state, |s| async move {
let mut buf = Vec::with_capacity(4096);
formatter.serialize(&mut buf, s, config_prefix.as_deref())?;
let file = AtomicFile::new(&config_path, AllowOverwrite);
tokio::task::spawn_blocking(move || file.write(|f| f.write_all(&buf)))
.await?
.with_context(|| format!("failed to write config: {config_path}"))?;
Ok::<_, anyhow::Error>(())
})
.await
.map(|((), report)| report)
.map_err(|e| match e {
WithEffectError::State(e) => UpsertError::State(e),
WithEffectError::Effect(e) => UpsertError::WriteConfig(e),
WithEffectError::EffectTimedOut(timeout) => UpsertError::WriteConfig(
anyhow::anyhow!("write config timed out after {timeout:?}"),
),
})
}
pub async fn replace_if_version(
&mut self,
expected_version: Version,
next_state: State,
) -> Result<ReplaceIfVersionResult, ReplaceIfVersionError>
where
Formatter: Clone,
{
let config_path = self.config_path.clone();
let config_prefix = self.config_prefix.clone();
let effect_config_path = config_path.clone();
let effect_config_prefix = config_prefix.clone();
let effect_formatter = self.formatter.clone();
let recovery_formatter = self.formatter.clone();View on GitHub (pinned to f7dbce2997)
Solutions
- Retry the upsert once the disk is responsive; the state change already committed, so a retry usually only re-triggers persistence.
- Verify the config directory is on a local, writable filesystem and not stalled (check I/O load, disk health, antivirus scans).
- Increase the write-config effect timeout budget if legitimate slow writes are expected in your deployment.
- Inspect logs for the WithEffectError::EffectTimedOut correlation to confirm the disk write task is hanging vs genuinely slow, and fix the underlying writer if it is deadlocked.
Example fix
// before: effect timeout too small for slow disk with_effect(write_config, Duration::from_millis(500)).await // after: allow realistic persist time with_effect(write_config, Duration::from_secs(5)).await
Defensive patterns
Strategy: retry
Validate before calling
// pre-check disk writability and budget before upsert let probe = tokio::fs::OpenOptions::new().append(true).open(&config_path).await?; assert!(timeout_budget >= Duration::from_secs(1), "write budget too small");
Type guard
fn is_write_timeout(err: &UpsertError) -> bool {
matches!(err, UpsertError::WriteConfig(e) if e.to_string().contains("write config timed out"))
} Try / catch
match manager.upsert(key, value).await {
Err(UpsertError::WriteConfig(e)) if e.to_string().contains("timed out") => {
tracing::warn!("state committed but persist timed out; retrying persist");
retry_persist().await?; // state is already current in memory
}
other => other?,
} Prevention
- Keep config files on local, healthy disks; avoid network/overlay filesystems for config storage.
- Set effect timeouts proportional to worst-case serialization+write size.
- Monitor disk latency and treat persist timeouts as alerts, not user-facing errors.
- Verify persistence with a follow-up read-back after a timeout before assuming data loss.
When it happens
Trigger: Calling `upsert` on the persistent state manager when the spawned config-write effect (disk write of the config file) exceeds its effect timeout; typically under heavy disk I/O, a slow/hung filesystem, or an overly tight timeout budget.
Common situations: Slow or nearly-full disks, network/overlay filesystems (WSL shares, network drives), antivirus interference on Windows, or large verge/clash config files making serialization+write exceed the timeout.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- application actor call timed out
- clash config actor call timed out
- session state actor call timed out
- Clash stream actor timed out
- state transaction dropped before commit or rollback complete
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/5cf9f9e837a088f4.
Report an issue: GitHub.