PowerShell Remoting with Invoke-Command: Run Health Checks on Multiple Servers

Administrative workstation remotely collecting health status from multiple connected Windows servers

What You’ll Learn

In this lesson, you will use PowerShell remoting with Invoke-Command to run the same health-check script on multiple Windows servers from one administrative machine.

  • How to target several computers with Invoke-Command
  • How to collect useful properties from remote machines
  • How to control concurrency with -ThrottleLimit
  • How to recognize connection failures and service-related problems

The Concept

Invoke-Command sends a PowerShell script block to one or more remote computers and runs it there. The commands inside the script block execute in the remote session, while the resulting objects are returned to your administrative machine.

This makes it useful for centralized administration. Instead of signing in to each server individually, you can collect operating system information, service status, disk usage, or other health indicators in one command.

A typical pattern looks like this:

Invoke-Command -ComputerName "SERVER01", "SERVER02" -ScriptBlock {
    # Commands in this block run on each remote server.
}

The remote computers must be configured for PowerShell remoting, and your account must have permission to connect to them. In a Windows domain, this commonly means that WinRM, firewall rules, authentication, and permissions have already been configured by an administrator.

Basic Example

The following health check collects the last boot time, available physical memory, and the status of two important services from several Windows servers.

$servers = "APP01", "DB01", "WEB01"

$healthResults = Invoke-Command -ComputerName $servers -ScriptBlock {
    $operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem

    $services = Get-Service -Name "WinRM", "W32Time" -ErrorAction SilentlyContinue
    $stoppedServices = $services |
        Where-Object Status -ne "Running" |
        Select-Object -ExpandProperty Name

    [pscustomobject]@{
        Server              = $env:COMPUTERNAME
        LastBoot            = $operatingSystem.LastBootUpTime
        FreeMemoryGB        = [math]::Round($operatingSystem.FreePhysicalMemory / 1MB, 2)
        ServicesNotRunning  = $stoppedServices -join ", "
    }
}

$healthResults |
    Sort-Object Server |
    Format-Table -AutoSize

Expected Output

The exact boot times and memory values depend on your servers. The following is representative output. An empty ServicesNotRunning value means both requested services were running or were not found.

Server LastBoot              FreeMemoryGB ServicesNotRunning
------ --------              ------------ ------------------
APP01  3/18/2026 8:42:11 AM         6.84
DB01   3/17/2026 11:05:47 PM        12.31 W32Time
WEB01  3/19/2026 6:20:03 AM         3.76

How the Code Works

An administrative machine passes a list of Windows servers to Invoke-Command, which runs the health-check script remotely on each server. Each server returns structured health data, while connection or service problems are reviewed separately before the administrative machine aggregates and formats the results.
Invoke-Command fans one health-check script out to multiple Windows servers, then returns structured objects for centralized filtering, sorting, and reporting.

$servers is an array of computer names. Passing that array to -ComputerName causes the script block to run on each listed computer.

Everything inside the script block runs remotely. Get-CimInstance reads the Win32_OperatingSystem class on the current server. Its FreePhysicalMemory property is reported in kilobytes; dividing by the PowerShell 1MB constant converts the numeric value to an approximate number of gigabytes.

The service query uses -ErrorAction SilentlyContinue so that a server without one of the named services does not produce a noisy error. Where-Object keeps services that are not running, and Select-Object -ExpandProperty Name extracts only their names.

The PSCustomObject gives each server a consistent result shape. This is preferable to returning unstructured text because the results can later be sorted, filtered, exported, or processed in another function.

$env:COMPUTERNAME is evaluated on the remote server, so it identifies which machine produced each result. By contrast, ordinary local variables are not automatically available inside a remote script block. If you need to pass a local value into the block, use the $using: scope modifier or a parameterized remote script block.

Another Example

This example focuses on local fixed disks. It uses -ThrottleLimit to limit the number of simultaneous remote operations and returns a record for each disk so that low-space volumes can be reviewed centrally.

$servers = "APP01", "DB01", "WEB01"

$diskResults = Invoke-Command -ComputerName $servers -ThrottleLimit 4 -ScriptBlock {
    Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType = 3" |
        ForEach-Object {
            $freePercent = [math]::Round(
                ($_.FreeSpace / $_.Size) * 100,
                1
            )

            [pscustomobject]@{
                Server          = $env:COMPUTERNAME
                Drive           = $_.DeviceID
                FreePercent     = $freePercent
                FreeSpaceGB     = [math]::Round($_.FreeSpace / 1GB, 1)
                NeedsAttention  = $freePercent -lt 15
            }
        }
}

$diskResults |
    Where-Object NeedsAttention |
    Sort-Object FreePercent |
    Format-Table -AutoSize

Here, the filtering happens on the administrative machine after the remote objects are returned. That keeps the remote script focused on collecting data and lets you change the reporting rule without changing what is collected.

Common Mistakes

  • Remoting is not enabled: A connection error usually means WinRM, firewall rules, DNS, authentication, or permissions need attention. Test a single server first with Test-WSMan -ComputerName APP01.
  • Using local variables inside the script block: A variable such as $servers is not automatically copied into the remote session. Pass required values explicitly or use $using:variableName.
  • Returning formatted text too early: Commands such as Format-Table create formatting data, not normal business objects. Format only after collecting and filtering results.
  • Assuming every server has the same services: A service name may be missing or may use a different configuration on another server. Use appropriate error handling and interpret missing services separately from stopped services.
  • Ignoring failures in a multi-server call: Successful servers can return results even when another server cannot be contacted. Review error output and consider adding explicit error records when building an operational report.

Try It Yourself

Extend the basic health check so that it also reports the number of logical processors on each server. Add a ProcessorCount property to the returned object using (Get-CimInstance -ClassName Win32_ComputerSystem).NumberOfLogicalProcessors, then include that property in the table output.

Challenge

Create a centralized health report for the servers APP01, DB01, and WEB01 with these requirements:

  • Run the check remotely with Invoke-Command.
  • Report the server name, whether the WinRM service is running, and the percentage of free space on the system drive.
  • Set a Status property to "Healthy" when WinRM is running and free space is at least 15 percent; otherwise set it to "Needs attention".
  • Sort the final results by Status and then Server.

Solution

$servers = "APP01", "DB01", "WEB01"

$healthReport = Invoke-Command -ComputerName $servers -ScriptBlock {
    $winRmService = Get-Service -Name "WinRM" -ErrorAction SilentlyContinue
    $systemDrive = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID = 'C:'"

    $freePercent = [math]::Round(
        ($systemDrive.FreeSpace / $systemDrive.Size) * 100,
        1
    )

    $winRmRunning = $winRmService.Status -eq "Running"
    $status = if ($winRmRunning -and $freePercent -ge 15) {
        "Healthy"
    }
    else {
        "Needs attention"
    }

    [pscustomobject]@{
        Server          = $env:COMPUTERNAME
        WinRMRunning    = $winRmRunning
        FreePercent     = $freePercent
        Status          = $status
    }
}

$healthReport |
    Sort-Object Status, Server |
    Format-Table -AutoSize

The script gathers both health signals on each remote server and creates one object per server. The conditional expression combines the two checks, while the final pipeline sorts and formats the returned objects on the administrative machine.

Key Takeaways

  • Invoke-Command runs a script block on one or more remote Windows computers.
  • Return structured objects with PSCustomObject so results remain easy to filter and sort.
  • Use remote variables such as $env:COMPUTERNAME when identifying the machine that produced a result.
  • Apply formatting commands such as Format-Table only after remote data has been collected.
  • For reliable administration, account for remoting configuration, missing services, connection failures, and concurrency limits.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top