windmill-labs/windmill · error

CreateToolhelp32Snapshot: {e}

Error message

CreateToolhelp32Snapshot: {e}

What it means

On Windows, resume_process enumerates all threads via CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD) to find and ResumeThread the child's threads (the child was started suspended). If the Win32 snapshot call fails, the error is wrapped into an io::Error with this message. Snapshot failure typically means the toolhelp subsystem could not be accessed for the calling process.

Source

Thrown at backend/windmill-worker/src/common.rs:830

        self.inner.try_wait()
    }
}

/// Resume all threads of a process created with CREATE_SUSPENDED. We create the child
/// suspended so it can be assigned to its job object before running any code; otherwise
/// a child that forks a helper at startup could create it before the assignment and
/// leave it outside the job (escaping KILL_ON_JOB_CLOSE reaping / the memory cap).
/// (Ported from process-wrap's resume_threads.)
#[cfg(windows)]
fn resume_process(pid: u32) -> Result<(), std::io::Error> {
    use windows::Win32::System::Diagnostics::ToolHelp::{
        CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
    };
    use windows::Win32::System::Threading::{OpenThread, ResumeThread, THREAD_SUSPEND_RESUME};

    unsafe {
        let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0).map_err(|e| {
            std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("CreateToolhelp32Snapshot: {e}"),
            )
        })?;
        let mut entry = THREADENTRY32 {
            dwSize: std::mem::size_of::<THREADENTRY32>() as u32,
            cntUsage: 0,
            th32ThreadID: 0,
            th32OwnerProcessID: 0,
            tpBasePri: 0,
            tpDeltaPri: 0,
            dwFlags: 0,
        };
        let mut res = Thread32First(snapshot, &mut entry);
        while res.is_ok() {
            if entry.th32OwnerProcessID == pid {
                if let Ok(thread) = OpenThread(THREAD_SUSPEND_RESUME, false, entry.th32ThreadID) {
                    ResumeThread(thread);

View on GitHub (pinned to e474e8803c)

Solutions

  1. Check the Win32 error code appended to the message (map Windows error codes, e.g. 5 = access denied) and fix the underlying permission.
  2. Run the worker under an account with normal process/thread access rights; avoid running inside heavily restricted sandboxes.
  3. Retry the job; transient snapshot allocation failures can clear under lower memory pressure.
  4. Update Windows/the windows crate if a known toolhelp regression applies.
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-call check possible for toolhelp snapshot availability; verify the process runs with standard user privileges on Windows before spawning suspended children.

Try / catch

match resume_process(pid) {
    Err(e) if e.to_string().contains("CreateToolhelp32Snapshot") => {
        log::warn!("thread snapshot failed, retrying once: {e}");
        std::thread::sleep(std::time::Duration::from_millis(50));
        resume_process(pid)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: start_child_process on Windows spawning a suspended child, then resume_process calling CreateToolhelp32Snapshot and the API returning a Win32 error (e.g. ERROR_ACCESS_DENIED or out-of-memory for the snapshot).

Common situations: Hardened/restricted Windows environments (job containers, service accounts without sufficient privileges), resource exhaustion creating a system-wide thread snapshot, antivirus or policy blocking toolhelp APIs.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/8b2dd9d02681dcae. Report an issue: GitHub.