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

  1. Initialize root: Initialize-KrRoot -Path $PSScriptRoot for predictable relative paths.
  2. Server & listener: New-KrServer; Add-KrEndpoint -Port 5000 -IPAddress Loopback.
  3. Declare shared variables BEFORE enabling configuration:
    • $Visits = [ConcurrentDictionary[string,int]]::new() stores counts across requests.
    • $Delay = 1 simulates work to trigger multiple runspaces.
  4. Apply config: Enable-KrConfiguration freezes the configuration graph.
  5. Map routes that use the shared variables:
    • GET /show returns the current visit count using $Visits.Count.
    • GET /visit sleeps for $Delay and increments $Visits using AddOrUpdate.
  6. Start: Start-KrServer.

Behavior contract

  • Inputs: two GET routes (/show, /visit). No body required.
  • Outputs: text/plain responses; /show reports current count; /visit increments 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.