How to Find the Process Locking a File Using PowerShell

You’re trying to delete or move a file, and Windows throws that frustrating “The file is in use by another process” error. You have no idea what’s holding it. Task Manager shows a dozen processes, and you’re left guessing. Sound familiar?

Good news — PowerShell gives you several solid ways to track down exactly which process has a lock on your file. In this tutorial, I’ll walk you through four practical methods to find the process locking a file using PowerShell. By the end, you’ll know exactly what to do the next time a locked file blocks your work.

When a program opens a file — say, Excel opens a spreadsheet, or an antivirus scanner reads a DLL — Windows gives that program a “handle” to that file. As long as the program holds that handle, the file is considered “in use.” Other processes (or you) can’t delete, rename, or overwrite it until that handle is released.

This is intentional. Windows does it to prevent data corruption. But it becomes a problem when the locking process crashes, hangs, or just forgets to let go.

Method 1: Use Get-Process with .Modules (Quick Check for DLLs)

This is the most native PowerShell approach. Every running process has a list of modules — executable files and DLLs it has loaded. You can loop through all processes and their modules and check whether any of them match your file path.

Here’s the script:

$lockedFile = "C:\MyApp\bin\myLibrary.dll"

Get-Process | ForEach-Object {
$process = $_
try {
$_.Modules | ForEach-Object {
if ($_.FileName -eq $lockedFile) {
[PSCustomObject]@{
ProcessName = $process.Name
PID = $process.Id
FileName = $_.FileName
}
}
}
} catch {
# Some system processes deny access; skip them silently
}
}

How it works:

  • Get-Process fetches all running processes
  • .Modules gives you every DLL/EXE that process has loaded into memory
  • We compare each module’s file path to our target file
  • If there’s a match, we print the process name and PID

Limitation: This method only catches files that are loaded as modules (DLLs, executables). If Excel has a .xlsx file open or Notepad has a .txt file open, this won’t catch it. For those, you need a different approach.

Check out Why Does Windows PowerShell Keep Popping Up?

Method 2: Use Sysinternals Handle.exe from PowerShell

This is the most reliable method for any file type. Microsoft’s Sysinternals suite includes a tiny command-line tool called handle.exe that can show every open file handle across all processes. It’s free, it’s from Microsoft, and it works beautifully inside PowerShell.

Step 1 – Download Handle.exe

You can download it manually from https://docs.microsoft.com/en-us/sysinternals/downloads/handle, or let PowerShell fetch it for you:

$tempDir = $env:TEMP
$handleZip = Join-Path $tempDir "Handle.zip"
$handleExe = Join-Path $tempDir "handle64.exe"

if (-not (Test-Path $handleExe)) {
Invoke-WebRequest -Uri "https://download.sysinternals.com/files/Handle.zip" -OutFile $handleZip
Expand-Archive -LiteralPath $handleZip -DestinationPath $tempDir -Force
}

# Accept the EULA silently via registry
reg.exe ADD "HKCU\Software\Sysinternals\Handle" /v EulaAccepted /t REG_DWORD /d 1 /f | Out-Null

Step 2 – Run it against your file

$lockedFile = "C:\Reports\Q1_Sales.xlsx"
$output = & $handleExe $lockedFile

if ($output -match "no matching handles found") {
Write-Host "No process is locking this file."
} else {
Write-Host "Locking process(es) found:"
$output
}

Sample output:

EXCEL.EXE     pid: 7432   type: File   5BC: C:\Reports\Q1_Sales.xlsx

That tells you exactly what you need: Excel (PID 7432) has the file open.

Step 3 – Parse it cleanly (optional)

If you want structured objects instead of raw text:

$regex = [regex]"(?<Name>\S+)\s+pid:\s+(?<PID>\d+)\s+type:\s+\S+\s+\S+:\s+(?<Path>.*)"

$output | ForEach-Object {
$match = $regex.Match($_)
if ($match.Success) {
[PSCustomObject]@{
ProcessName = $match.Groups["Name"].Value
PID = [int]$match.Groups["PID"].Value
FilePath = $match.Groups["Path"].Value
}
}
}

This gives you a clean PowerShell object you can pipe into other commands or export to CSV.

Read PowerShell Get-WindowsAutoPilotInfo

Method 3: Use NtQueryInformationFile via .NET P/Invoke (No Third-Party Tools)

If you can’t install or download anything on a locked-down server, this method is for you. It calls a low-level Windows API (NtQueryInformationFile) directly from PowerShell using .NET P/Invoke. It’s a bit more code, but it has zero dependencies.

Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

public static class FileUtils {
[StructLayout(LayoutKind.Sequential)]
private struct IO_STATUS_BLOCK {
public IntPtr Status;
public IntPtr Information;
}

[StructLayout(LayoutKind.Sequential)]
public struct FILE_PROCESS_IDS_USING_FILE_INFO {
public ulong NumberOfProcessIds;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public ulong[] ProcessIdList;
}

[DllImport("ntdll.dll")]
private static extern int NtQueryInformationFile(
SafeFileHandle FileHandle,
ref IO_STATUS_BLOCK IoStatusBlock,
IntPtr FileInfo,
uint Length,
int FileInfoClass);

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern SafeFileHandle CreateFile(
string lpFileName, uint dwAccess, uint dwShareMode,
IntPtr lpSecurityAttributes, uint dwCreationDisp,
uint dwFlagsAndAttributes, IntPtr hTemplateFile);

public static ulong[] GetProcessIds(string path) {
var ioStatus = new IO_STATUS_BLOCK();
var info = new FILE_PROCESS_IDS_USING_FILE_INFO();
using (var handle = CreateFile(path, 0x80000000, 0x7, IntPtr.Zero, 3, 0, IntPtr.Zero)) {
if (handle.IsInvalid) return new ulong[0];
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(info));
if (NtQueryInformationFile(handle, ref ioStatus, ptr, (uint)Marshal.SizeOf(info), 47) == 0) {
info = Marshal.PtrToStructure<FILE_PROCESS_IDS_USING_FILE_INFO>(ptr);
}
Marshal.FreeHGlobal(ptr);
}
if (info.NumberOfProcessIds == 0) return new ulong[0];
var ids = new ulong[info.NumberOfProcessIds];
Array.Copy(info.ProcessIdList, ids, (int)info.NumberOfProcessIds);
return ids;
}
}
"@

$filePath = "C:\Logs\app.log"
$pids = [FileUtils]::GetProcessIds($filePath)

if ($pids.Count -eq 0) {
Write-Host "No process is locking $filePath"
} else {
foreach ($pid in $pids) {
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
Write-Host "Locking Process: $($proc.Name) (PID: $pid)"
}
}

This works for any file type — logs, Word documents, Excel files, config files — you name it. The trade-off is that the Add-Type compilation adds a couple of seconds the first time you run it.

Check out Check Who Modified a File Last in Windows Using PowerShell

Method 4: Kill the Locking Process Automatically

Sometimes you don’t just want to find the culprit — you want to stop it. Here’s how to combine the Handle.exe approach with Stop-Process to free the file in one shot:

$handleExe = Join-Path $env:TEMP "handle64.exe"
$lockedFile = "C:\Deploy\app.config"

$output = & $handleExe $lockedFile

$regex = [regex]"(?<Name>\S+)\s+pid:\s+(?<PID>\d+)"
$output | ForEach-Object {
$match = $regex.Match($_)
if ($match.Success) {
$pid = [int]$match.Groups["PID"].Value
$name = $match.Groups["Name"].Value
Write-Host "Stopping process: $name (PID: $pid)"
Stop-Process -Id $pid -Force
}
}

Before you run this — make sure you actually want to kill those processes. Stopping something like svchost.exe without checking first can take down services you didn’t intend to stop. Always review the output of Method 2 before automating the kill step.

Read Delete User Profiles Using PowerShell in Windows 11

Quick Reference: Which Method Should You Use?

SituationBest Method
File is a DLL or EXEMethod 1 (Get-Process .Modules)
Any file type, internet access availableMethod 2 (Handle.exe)
Locked-down server, no downloads allowedMethod 3 (.NET P/Invoke)
Automated pipeline, need to auto-releaseMethod 4 (Handle.exe + Stop-Process)

Tips to Avoid File Lock Issues Going Forward

  • Always close file handles explicitly in your scripts. If you open a FileStream in PowerShell, wrap it in a try/finally block and call .Close() or .Dispose().
  • Use -ReadOnly or shared access modes when you only need to read a file — this reduces the chance of blocking other processes.
  • Check before you act. Before trying to delete or overwrite a critical file in a script, run a quick lock check first so you don’t get a cryptic error mid-deployment.
  • Task Manager’s Resource Monitor is a handy GUI fallback. Open it, go to the CPU tab, and type the filename in the “Search Handles” box — it’ll show you the locking process immediately without any scripting.

Wrapping Up

In this tutorial, I have explained how to find the process locking a file using PowerShell using various methods. Once you know how to query process handles — whether through .Modules, Handle.exe, or a .NET API call — the mystery disappears fast. I personally keep a saved version of Method 2 in my PowerShell profile so I can call it any time with a single line. It saves a lot of headscratching during deployments.

Pick the method that fits your environment and run with it. You’ll have the locked file’s owner identified in seconds. Do let me know if you still have any questions in the comments below.

You may also like:

100 PowerShell cmdlets download free

100 POWERSHELL CMDLETS E-BOOK

FREE Download an eBook that contains 100 PowerShell cmdlets with complete script and examples.