When you launch a new PowerShell session, you might have some environmental configuration that is performed. For example, you might customize your shell prompt, or pre-load some Powershell modules.
Typically this is done by configuring your PowerShell profile script. Over time, you might find that you’ve added more and more functionality to your profile script. This added functionality can slow down your PowerShell startup time, resulting in a poor user experience.
At this point, you might start looking for things that you can remove from your profile script, to speed it up. Before you go that route, let me propose an alternative solution.
My Specific Scenario
I use the AWS PowerShell module to perform automation tasks against AWS cloud resources. It’s essential that I have up-to-date temporary credentials for AWS IAM, and have the AWS PowerShell module loaded. Hence, in my PowerShell profile script, I have some code to automatically refresh my temporary credentials and import the AWS PowerShell module.
Importing the monolithic AWSPowerShell.NetCore
module is very slow, as it contains thousands of commands across the entire AWS service portfolio.
NOTE: There is a non-monolithic version of the AWS PowerShell SDK, that splits each AWS service into separate PowerShell modules, but that is challenging to keep updated.
To test the performance of importing the AWSPowerShell.NetCore
module, I run a new PowerShell session while skipping my profile script.
pwsh -NoProfile
Next, I use the built-in Measure-Command
command to test the module load time.
Measure-Command -Expression { Import-Module AWSPowerShell.NetCore }
On my high-end developer workstation, with an AMD Ryzen 3900X, 32 GB DDR4, and a 1TB Intel 660p NVMe SSD, this module takes 10.19 seconds to load.
Who wants to wait more than 10 seconds every time they launch a PowerShell session? Not me! That doesn’t even include the extra time that it would take to refresh my AWS IAM credentials.
Let’s explore how we can fix this startup performance issue, without sacrificing our objective.
Asynchronous Code Execution in PowerShell
PowerShell has a few different ways of handling asynchronous code execution. First of all, you can use PowerShell Background Jobs, but those are kind of slow. Instead, you can use PowerShell Runspaces.
First, construct a PowerShell script manager object.
$MyScript = [powershell]::Create()
Once you’ve created the PowerShell script manager, you can add individual statements, or an entire PowerShell script to it. This is where you would place the code to refresh your AWS IAM credentials, as I described earlier.
The specific implementation of this function is outside the scope of this article. If you’re interested in how I accomplished this, feel free to reach out to me on Twitter. For now, we’ll just call Start-Sleep
to emulate the delay of loading the AWSPowerShell.NetCore
module.
$MyScript.AddScript(@'
# Define a function to update IAM credentials
function Update-AWSIAMCredentials {
# Do stuff here
Start-Sleep -Seconds 10
}
# Call the function
Update-AWSIAMCredentials
'@)
Once you’ve added the script you want to execute asynchronously, create a PowerShell Runspace to execute it in.
$Runspace = [runspacefactory]::CreateRunspace()
Now you need to associate the Runspace with the PowerShell script manager. To do that, simply assign the Runspace to the Runspace
property of the script manager. This tells the script where to run, but it still hasn’t been invoked.
$MyScript.Runspace = $Runspace
Register an Event Handler to Execute After Script
Before we actually invoke the script in the new Runspace we’ve created, let’s register an event handler to tell our “main” PowerShell Runspace what to do after the child Runspace completes successfully.
PowerShell has a built-in command called Register-ObjectEvent
. This command allows you to handle events on .NET objects. Both the Runspace and the PowerShell script manager objects are .NET objects. Each of these objects has unique events that we can hook into.
We’ll get to this in just a minute.
Before that, how do you know which events are exposed on these objects? Just pipe both variables into the Get-Member
command in PowerShell to find out!
$MyScript, $Runspace | Get-Member -MemberType Event
You should see output similar to below.
TypeName: System.Management.Automation.PowerShell
Name MemberType Definition
---- ---------- ----------
InvocationStateChanged Event System.EventHandler`1[System.Management.Automation.PSInvocationSta…
TypeName: System.Management.Automation.Runspaces.LocalRunspace
Name MemberType Definition
---- ---------- ----------
AvailabilityChanged Event System.EventHandler`1[System.Management.Automation.Runspaces.Runspace…
StateChanged Event System.EventHandler`1[System.Management.Automation.Runspaces.Runspace
For this scenario, we’ll use the InvocationStateChanged
event on the PowerShell script manager to detect when execution has completed.
Now that we know which event we want to hook into, we can go back and register for the event using Register-ObjectEvent
. We need just a few, key input parameters.
-InputObject
points to the object that exposes the event we wish to hook into-EventName
is a string containing the name of the event on the-InputObject
that we are hooking into-Action
parameter contains a PowerShell ScriptBlock that will execute in our “main” Runspace after the child Runspace completes
$null = Register-ObjectEvent -InputObject $PowerShell -EventNameInvocationStateChanged -Action {
Import-Module -Name AWSPowerShell.NetCore
Set-AWSCredential -ProfileName cbt
}
You might notice that I prefaced the call to Register-ObjectEvent
with $null =
. That’s because the command will return a reference to the event registration, which I don’t need a handle to. Hence, I just discard the output by assigning the result to $null
.
Check Your Results
Save all of the code that we’ve discussed into your $profile.CurrentUserAllHosts
file. You can use vim
or Visual Studio Code as your editor, or any other text editor you want.
The next time you fire up a PowerShell session, you should notice that it loads almost immediately. You can start working in your PowerShell terminal right away, however the background credential refresh task is still executing. If you inspect the $MyScript
variable’s InvocationStateInfo
child property ($ps
in the screenshot), it should indicate that it has completed successfully.

I hope that this has taught you something about using PowerShell Runspaces, specifically in the context of optimizing performance of your PowerShell “profile” script. If you have any questions, feel free to leave a comment or find me on Twitter. Also, you can subscribe to my YouTube channel, where I periodically post new videos.