Using Shared Variables
Share in-memory state (counters, caches, configuration) by defining variables before Enable-KrConfiguration so route script blocks can reference them at execution time.
Prerequisites: see Introduction.
Full source
File: pwsh/tutorial/examples/4.1-Shared-Variables.ps1
<#
Sample Kestrun Server on how to use shared variables.
This example demonstrates how to use shared variables in a Kestrun server.
FileName: 4.1-Shared-Variables.ps1
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# Initialize Kestrun root directory
# the default value is $PWD
# This is recommended in order to use relative paths without issues
Initialize-KrRoot -Path $PSScriptRoot
# Create a new Kestrun server
New-KrServer -Name "Simple Server"
# Add a listener using provided parameters
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# Add the PowerShell runtime
# Shared variables
$Visits = [System.Collections.Concurrent.ConcurrentDictionary[string, int]]::new()
# Shared variable for delay to simulate processing time and force the use of multiple runspaces
$Delay = 1
# Enable Kestrun configuration
Enable-KrConfiguration
# Show the current visit count
Add-KrMapRoute -Server $server -Verbs Get -Pattern '/show' -ScriptBlock {
# $Visits is available
Write-KrTextResponse -InputObject "[Runspace: $([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name)] Visits so far: $($Visits.Count)" -StatusCode 200
}
# Route: GET /visit
Add-KrMapRoute -Server $server -Verbs Get -Pattern '/visit' -ScriptBlock {
# Simulate some delay
Start-Sleep -Seconds $Delay
# increment the injected variable
$Visits.AddOrUpdate("Count", 1, { param($k, $v) $v + 1 })
Write-KrTextResponse -InputObject "[Runspace: $([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name)] Incremented to $($Visits.Count)" -StatusCode 200
}
# Start the server asynchronously
Start-KrServer
Step-by-step
- Initialize root:
Initialize-KrRoot -Path $PSScriptRootfor predictable relative paths. - Server & listener:
New-KrServer;Add-KrEndpoint -Port 5000 -IPAddress Loopback. - Declare shared variables BEFORE enabling configuration:
$Visits = [ConcurrentDictionary[string,int]]::new()stores counts across requests.$Delay = 1simulates work to trigger multiple runspaces.
- Apply config:
Enable-KrConfigurationfreezes the configuration graph. - Map routes that use the shared variables:
- GET
/showreturns the current visit count using$Visits.Count. - GET
/visitsleeps for$Delayand increments$VisitsusingAddOrUpdate.
- GET
- Start:
Start-KrServer.
Behavior contract
- Inputs: two GET routes (
/show,/visit). No body required. - Outputs: text/plain responses;
/showreports current count;/visitincrements then reports. - Shared state: variables declared pre-Enable are captured and shared across request runspaces.
How sharing works
Route script blocks capture the parent (script) scope at registration time. Variables created before Enable-KrConfiguration are available inside each runspace used to handle requests. Mutable reference types remain shared — all routes see the same underlying instance.
Choose thread-safe types
Use ConcurrentDictionary (or other thread-safe collections) for shared mutations. Avoid plain hashtables for concurrent writes. Primitive value types (like an [int] variable) are copied into each runspace rather than shared by reference, so incrementing a plain $Counter won’t reflect across runspaces. Wrap numeric state in a shared mutable object (e.g., store under a key in a ConcurrentDictionary) if it must be updated concurrently.
Access patterns
# Read current value
$count = $Visits.Count
# Naive increment (ok for demo, not atomic for mixed operations):
$Visits.Count++
# Keyed counter (atomic style)
$null = $Visits.AddOrUpdate('total', 1, { param($k,$v) $v + 1 })
# Atomic increment loop ("clunky" style)
do {
$oldVal = $Visits['total']
$newVal = $oldVal + 1
} while (-not $Visits.TryUpdate('total', $newVal, $oldVal))
Common pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Plain hashtable for writes | Lost / inconsistent updates | Use ConcurrentDictionary or locking |
| Variable defined too late | $null inside routes | Move declaration before Enable-KrConfiguration |
| Variable name shadowed | Local variable with same name | Use a different local name or explicit scope |
| Heavy synchronous logic | Slow responses | Offload to background job / queue |
Try it
Follow these quick steps to prove the variable sharing behavior and observe concurrent increments.
1. Save & run the sample
Copy the contents of pwsh/tutorial/examples/4.1-Shared-Variables.ps1 into a new file in an empty workspace folder (e.g., shared-vars.ps1) and start it:
pwsh .\shared-vars.ps1
You should see startup log lines similar to:
info: Kestrun[0] Listening on http://127.0.0.1:5000
Leave this session running.
2. In a second terminal: basic requests
PowerShell:
Invoke-WebRequest http://127.0.0.1:5000/show | Select-Object -Expand Content
Invoke-WebRequest http://127.0.0.1:5000/visit | Select-Object -Expand Content
Invoke-WebRequest http://127.0.0.1:5000/visit | Select-Object -Expand Content
Invoke-WebRequest http://127.0.0.1:5000/show | Select-Object -Expand Content
curl (optional):
curl http://127.0.0.1:5000/show
curl http://127.0.0.1:5000/visit
curl http://127.0.0.1:5000/visit
curl http://127.0.0.1:5000/show
Example output progression (values will differ):
Visits so far: 0
Incremented to 1
Incremented to 2
Visits so far: 2
3. Concurrency test
Issue multiple concurrent /visit requests to trigger usage of multiple runspaces. Each job hits the shared $Visits dictionary.
(1..40 | ForEach-Object { Start-Job { Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:5000/visit' } } |
Receive-Job -Wait -AutoRemoveJob | Select-Object -Expand Content) | Select-Object -First 5
Invoke-WebRequest http://127.0.0.1:5000/show | Select-Object -Expand Content
You will see mixed runspace names in the returned lines, confirming multiple worker runspaces share the same $Visits instance.
4. Observe runspace names
If you call /visit a few more times you will likely see different [Runspace: <name>] prefixes. They demonstrate the script blocks are executing in different runspaces, yet the count keeps increasing globally.
5. Reset behavior
Stop the server (Ctrl+C). Restarting the script resets the in-memory state (the dictionary is rebuilt). For persistence, externalize state to a file or database.
6. Quick one-liner sanity loop (optional)
1..5 | ForEach-Object { Invoke-WebRequest http://127.0.0.1:5000/visit | Select-Object -Expand Content }
You should see monotonically increasing counts.
Troubleshooting quick checks
| Issue | Quick check | Fix |
|---|---|---|
| Always 0 | Hitting /show only | Call /visit first |
$Visits null | Declared after Enable-KrConfiguration | Move declaration earlier |
| No concurrent increment speedup | $Delay too small | Increase $Delay temporarily (e.g., 2–3) |
Advanced: custom services
You can assign a custom class instance (cache, metrics aggregator, etc.) to a variable pre-configuration and expose its methods through routes. Ensure internal synchronization if it mutates shared state.
Best practices
- Keep state minimal; externalize durable data.
- Prefer thread-safe / immutable structures.
- Validate inputs before mutating shared objects.
- Avoid large synchronized regions; keep per-request critical work small.
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
Variable is $null | Declared after configuration | Declare earlier |
| Inconsistent data | Non-thread-safe structure | Use thread-safe type |
| High CPU | Busy loop / heavy work | Throttle or move off request path |
| Memory growth | Unbounded keys | Evict or cap size |
References
Previous / Next
Go back to Response Caching or continue to Logging.