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

  1. Retry the upsert once the disk is responsive; the state change already committed, so a retry usually only re-triggers persistence.
  2. Verify the config directory is on a local, writable filesystem and not stalled (check I/O load, disk health, antivirus scans).
  3. Increase the write-config effect timeout budget if legitimate slow writes are expected in your deployment.
  4. 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

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.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/5cf9f9e837a088f4. Report an issue: GitHub.