windmill-labs/windmill · error

CreateToolhelp32Snapshot

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.

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.

Appendix: 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)