Managing Shared State
Use the Kestrun shared state cmdlets (Set-KrSharedState, Get-KrSharedState, Remove-KrSharedState) to create and manage thread-safe, cross-runspace state accessible from all routes and languages (PowerShell, C#, VB.NET).
Prerequisites: see Introduction.
Full source
File: pwsh/tutorial/examples/4.2-Shared-State.ps1
<#
Sample Kestrun Server demonstrating shared state management.
This example shows how to use Set-KrSharedState, Get-KrSharedState, and Remove-KrSharedState cmdlets.
FileName: 4.2-Shared-State.ps1
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# Initialize Kestrun root directory
Initialize-KrRoot -Path $PSScriptRoot
# Create a new Kestrun server
New-KrServer -Name 'Shared State Server'
# Add a listener using provided parameters
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# Define shared state using Set-KrSharedState
# This creates thread-safe, cross-runspace accessible state
Set-KrSharedState -Name 'Visits' -Value @{ Count = 0 } -ThreadSafe
Set-KrSharedState -Name 'Config' -Value @{ MaxVisits = 100; AppName = 'SharedStateDemo' } -ThreadSafe
Set-KrSharedState -Name 'StartTime' -Value @{ Time = (Get-Date) } -ThreadSafe
# Enable Kestrun configuration
Enable-KrConfiguration
# Route: GET /info - Show configuration from shared state
Add-KrMapRoute -Verbs Get -Pattern '/info' -ScriptBlock {
$config = Get-KrSharedState -Name 'Config'
$startTime = Get-KrSharedState -Name 'StartTime'
$uptime = (Get-Date) - $startTime.Time
$response = @{
appName = $config.AppName
maxVisits = $config.MaxVisits
uptimeSeconds = [math]::Round($uptime.TotalSeconds, 2)
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
}
Write-KrJsonResponse -InputObject $response -StatusCode 200
}
# Route: GET /visits - Show current visit count
Add-KrMapRoute -Verbs Get -Pattern '/visits' -ScriptBlock {
$visits = Get-KrSharedState -Name 'Visits'
Write-KrJsonResponse -InputObject @{
count = $visits.Count
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
} -StatusCode 200
}
# Route: POST /visit - Increment visit counter
Add-KrMapRoute -Verbs Post -Pattern '/visit' -ScriptBlock {
$visits = Get-KrSharedState -Name 'Visits'
$config = Get-KrSharedState -Name 'Config'
Start-Sleep -Seconds 2 # Simulate some processing delay
# Atomic increment
Update-KrSynchronizedCounter -Table $visits -Key 'Count' -By 1
$response = @{
count = $visits.Count
maxVisits = $config.MaxVisits
remaining = $config.MaxVisits - $visits.Count
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
}
Write-KrJsonResponse -InputObject $response -StatusCode 200
}
# Route: POST /reset - Reset visit counter
Add-KrMapRoute -Verbs Post -Pattern '/reset' -ScriptBlock {
$visits = Get-KrSharedState -Name 'Visits'
$visits.Count = 0
Write-KrJsonResponse -InputObject @{
message = 'Visit counter reset'
count = 0
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
} -StatusCode 200
}
# Route: PUT /config - Update configuration
Add-KrMapRoute -Verbs Put -Pattern '/config' -ScriptBlock {
$body = Get-KrRequestBody
$config = Get-KrSharedState -Name 'Config'
if ($body.maxVisits) {
$config.MaxVisits = [int]$body.maxVisits
}
if ($body.appName) {
$config.AppName = $body.appName
}
Write-KrJsonResponse -InputObject @{
message = 'Configuration updated'
config = $config
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
} -StatusCode 200
}
# Route: DELETE /state/{name} - Remove shared state variable
Add-KrMapRoute -Verbs Delete -Pattern '/state/{name}' -ScriptBlock {
$name = Get-KrRequestRouteParam -Name 'name'
if ($name -in @('Visits', 'Config', 'StartTime')) {
Write-KrJsonResponse -InputObject @{
error = 'Cannot remove core state variables'
name = $name
} -StatusCode 403
return
}
$removed = Remove-KrSharedState -Name $name
if ($removed) {
Write-KrJsonResponse -InputObject @{
message = 'State variable removed'
name = $name
} -StatusCode 200
} else {
Write-KrJsonResponse -InputObject @{
error = 'State variable not found'
name = $name
} -StatusCode 404
}
}
# Route: GET /state/{name} - Get specific shared state value
Add-KrMapRoute -Verbs Get -Pattern '/state/{name}' -ScriptBlock {
$name = Get-KrRequestRouteParam -Name 'name'
$value = Get-KrSharedState -Name $name
if ($null -eq $value) {
Write-KrJsonResponse -InputObject @{
error = 'State variable not found'
name = $name
} -StatusCode 404
return
}
Write-KrJsonResponse -InputObject @{
name = $name
value = $value
type = $value.GetType().FullName
runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
} -StatusCode 200
}
# Start the server
Start-KrServer
Step-by-step
- Initialize root:
Initialize-KrRoot -Path $PSScriptRootfor predictable relative paths. - Server & listener:
New-KrServer;Add-KrEndpoint -Port 5000 -IPAddress Loopback. - Define shared state using
Set-KrSharedState:Set-KrSharedState -Name 'Visits' -Value @{ Count = 0 }— thread-safe visit counterSet-KrSharedState -Name 'Config' -Value @{ MaxVisits = 100; AppName = 'SharedStateDemo' }— configurationSet-KrSharedState -Name 'StartTime' -Value (Get-Date) -AllowsValueType— server start time
- Apply config:
Enable-KrConfigurationfreezes the configuration graph. - Map routes that interact with shared state:
GET /info— retrieve configuration and uptimeGET /visits— show current visit countPOST /visit— increment visit counter (atomic viaUpdate-KrSynchronizedCounter)POST /reset— reset visit counter to zeroPUT /config— update configuration valuesDELETE /state/{name}— remove a shared state variableGET /state/{name}— retrieve a specific shared state value
- Start:
Start-KrServer.
Behavior contract
- Inputs: REST API with JSON request/response bodies.
- Outputs: JSON responses with status codes (200, 403, 404).
- Shared state: Variables created with
Set-KrSharedStateare accessible across all request runspaces and languages.
How shared state works
The Kestrun shared state system provides a thread-safe, centralized store for application-wide variables:
Key characteristics
- Thread-safe: All operations are synchronized to prevent race conditions.
- Cross-runspace: State is shared across all PowerShell runspaces handling requests.
- Cross-language: Accessible from PowerShell, C#, and VB.NET routes.
- Server-scoped: Each server instance has its own isolated shared state.
- Global option: Use
-Globalparameter for process-wide shared state across multiple servers.
Cmdlet overview
| Cmdlet | Purpose | When to use |
|---|---|---|
Set-KrSharedState | Create or update shared variable | Before Enable-KrConfiguration for initialization; in routes for updates |
Get-KrSharedState | Retrieve shared variable value | In routes to access shared data |
Remove-KrSharedState | Delete shared variable | When cleaning up or removing state |
Usage patterns
Initialize state before configuration:
Set-KrSharedState -Name 'Counter' -Value @{ Count = 0 }
Enable-KrConfiguration
Read state in routes:
Add-KrMapRoute -Verbs Get -Pattern '/count' -ScriptBlock {
$counter = Get-KrSharedState -Name 'Counter'
Write-KrJsonResponse @{ count = $counter.Count }
}
Update state in routes:
Add-KrMapRoute -Verbs Post -Pattern '/increment' -ScriptBlock {
$counter = Get-KrSharedState -Name 'Counter'
# Atomic increment to avoid lost updates under concurrency
Update-KrSynchronizedCounter -Table $counter -Key 'Count' -By 1
Write-KrJsonResponse @{ count = $counter.Count }
}
Atomic numeric increments
For counters under high concurrency you can lose updates using $counter.Count++ because multiple runspaces can read the same value then write back overlapping results (classic read‑modify‑write race). Use the atomic helper to guarantee correctness:
Add-KrMapRoute -Verbs Post -Pattern '/increment-atomic' -ScriptBlock {
$counter = Get-KrSharedState -Name 'Counter'
# Performs an atomic increment on the 'Count' key
Update-KrSynchronizedCounter -Table $counter -Key 'Count' -By 1
Write-KrJsonResponse @{ count = $counter.Count }
}
Update-KrSynchronizedCounter internally uses locking via Monitor.Enter/Exit for thread-safe increments, ensuring each request contributes exactly one increment even when dozens of runspaces hit the route simultaneously.
When to prefer it:
- Hot counters (frequent updates per second)
- Metrics / rate limiting logic
- Any place inaccurate totals would mislead downstream logic
When simple ++ is acceptable:
- Low traffic / demo scenarios
- Non-critical values where occasional drift is tolerable
Remove state:
Remove-KrSharedState -Name 'TempCache'
Advantages over manual variables
| Aspect | Manual Variables (4.1) | Shared State Cmdlets (4.2) |
|---|---|---|
| Thread safety | Must use thread-safe types | Collection storage only; value mutations require explicit locking or atomic helpers |
| Value types | Copied per runspace | Can be shared with -AllowsValueType |
| Cross-language | PowerShell only | Works in C#/VB.NET too |
| Runtime updates | Limited | Full create/read/update/delete |
| Discoverability | Variable scope rules | Explicit get/set API |
Try it
Follow these steps to explore the shared state management features.
1. Save & run the sample
Copy pwsh/tutorial/examples/4.2-Shared-State.ps1 into a file (e.g., shared-state.ps1) and start it:
pwsh .\shared-state.ps1
You should see startup log lines:
info: Kestrun[0] Listening on http://127.0.0.1:5000
Leave this session running.
2. Check initial state
PowerShell:
Invoke-RestMethod -Uri http://127.0.0.1:5000/info | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/visits | ConvertTo-Json
Expected output:
{
"appName": "SharedStateDemo",
"maxVisits": 100,
"uptimeSeconds": 2.45,
"runspace": "Runspace1"
}
{
"count": 0,
"runspace": "Runspace2"
}
3. Increment visits
1..5 | ForEach-Object {
Invoke-RestMethod -Uri http://127.0.0.1:5000/visit -Method Post | ConvertTo-Json
}
Example output:
{
"count": 1,
"maxVisits": 100,
"remaining": 99,
"runspace": "Runspace3"
}
...
4. Concurrent updates
Test thread-safety with concurrent requests:
(1..20 | ForEach-Object {
Start-Job { Invoke-RestMethod -Uri http://127.0.0.1:5000/visit -Method Post }
} | Receive-Job -Wait -AutoRemoveJob).count | Measure-Object -Sum
Verify the final count matches:
Invoke-RestMethod -Uri http://127.0.0.1:5000/visits
The count should be accurate with no lost increments.
5. Update configuration
$body = @{ maxVisits = 200; appName = 'UpdatedDemo' } | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/config -Method Put -Body $body -ContentType 'application/json' | ConvertTo-Json
Verify the update:
Invoke-RestMethod -Uri http://127.0.0.1:5000/info | ConvertTo-Json
6. Reset counter
Invoke-RestMethod -Uri http://127.0.0.1:5000/reset -Method Post | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/visits | ConvertTo-Json
7. Query specific state
Invoke-RestMethod -Uri http://127.0.0.1:5000/state/Config | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/state/StartTime | ConvertTo-Json
8. Clean up
Stop the server (Ctrl+C). State is reset on restart.
Advanced patterns
Global shared state
Use -Global to share state across multiple Kestrun server instances in the same process:
Set-KrSharedState -Global -Name 'ProcessMetrics' -Value @{ TotalRequests = 0 }
# Accessible from any server instance:
$metrics = Get-KrSharedState -Global -Name 'ProcessMetrics'
Value types with -AllowsValueType
By default, shared state requires reference types. For value types (int, bool, etc.), use -AllowsValueType:
Set-KrSharedState -Name 'RequestCount' -Value 0 -AllowsValueType
Note: Value types are boxed and synchronized, but prefer reference types (hashtables, objects) for better ergonomics.
Custom classes
Store custom PowerShell class instances:
class AppMetrics {
[int]$RequestCount = 0
[datetime]$LastRequest = [datetime]::MinValue
[void]RecordRequest() {
$this.RequestCount++
$this.LastRequest = Get-Date
}
}
Set-KrSharedState -Name 'Metrics' -Value ([AppMetrics]::new())
# In routes:
$metrics = Get-KrSharedState -Name 'Metrics'
$metrics.RecordRequest()
Best practices
- Initialize early: Call
Set-KrSharedStatebeforeEnable-KrConfigurationfor startup state. - Use reference types: Hashtables and custom objects work best; avoid plain value types unless using
-AllowsValueType. - Use atomic counter helper: Prefer
Update-KrSynchronizedCounterfor numeric increments to avoid lost updates. - Validate inputs: Check for null and validate data before updating shared state.
- Keep state minimal: Store only what’s needed; externalize large or persistent data.
- Document state schema: Comment expected structure and usage patterns.
- Consider global vs server scope: Use server-scoped (default) for isolated instances;
-Globalfor cross-server state.
Common pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Null reference | $null when accessing state | Verify name is correct and state was set |
| State not shared | Each runspace has different value | Use Set-KrSharedState, not plain variables |
| Lost updates | Concurrent updates missing | State is thread-safe; check logic for overwrites |
| Value type confusion | Changes not reflected | Use reference types or -AllowsValueType |
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
Variable is $null | Never set or wrong name | Verify Set-KrSharedState was called |
| State not updating | Overwriting reference | Mutate properties, don’t reassign variable |
| Memory growth | Unbounded keys/collections | Implement eviction or size limits |
| Cross-server state not working | Using server scope | Add -Global to both set and get |
Comparison with 4.1 Shared Variables
The previous tutorial (4.1 Shared Variables) demonstrated manual variable sharing by declaring variables before Enable-KrConfiguration. This tutorial introduces the recommended approach using dedicated cmdlets:
When to use manual variables (4.1):
- Rapid prototyping or simple demos
- PowerShell-only routes
- Variables already thread-safe (e.g.,
ConcurrentDictionary)
When to use shared state cmdlets (4.2):
- Production code requiring cross-language support
- Runtime state management (create/update/delete)
- Need for global state across server instances
- Value type sharing
- Better discoverability and maintainability
References
- Set-KrSharedState
- Get-KrSharedState
- Remove-KrSharedState
- Enable-KrConfiguration
- Add-KrMapRoute
- New-KrServer
- Start-KrServer
Previous / Next
Go back to Shared Variables or continue to Logging.
See also
For deeper architectural details (implicit vs explicit, atomic patterns, runspace lifecycle) see the Shared State & Runspaces Guide and Tutorial 4.1 for implicit variable sharing.