Deterministic Randomness in PowerShell: Seed Once, Reuse the Generator
Randomness is great for sampling, shuffling, test data, and demos—until you need to reproduce a result. Deterministic randomness solves this by using a fixed seed so that the same operations yield the same outputs every time. In PowerShell, the key is simple: seed once, then reuse the generator. In this post, you’ll learn practical patterns for repeatable tests, stable demos, predictable sampling, and easier debugging—without sacrificing clarity or performance.
The Core Pattern: Seed Once, Reuse the Generator
Why this matters
Deterministic randomness lets you verify behavior in CI/CD, produce stable screencasts, and debug hard-to-repro issues. If you re-seed for every call, you’ll often get the same value repeatedly, which defeats the purpose of randomness and makes tests misleading.
Correct approach: one generator, many draws
Use a single [System.Random] instance initialized with a fixed seed and reuse it for the entire operation or test run.
$rng = [System.Random]::new(12345)
$nums1 = 1..5 | ForEach-Object { $rng.Next(1, 100) }
$rng2 = [System.Random]::new(12345)
$nums2 = 1..5 | ForEach-Object { $rng2.Next(1, 100) }
$items = @('alpha','beta','gamma','delta')
$r = [System.Random]::new(42)
$shuf = $items | Sort-Object { $r.Next() }
[pscustomobject]@{
Repeatable = ($nums1 -join ',') -eq ($nums2 -join ',')
Numbers = ($nums1 -join ', ')
Shuffle = ($shuf -join ' ')
}
This produces the same sequence and the same shuffle every run because the seed and usage are consistent.
Anti-pattern: reseeding inside a loop
Re-seeding for each element restarts the sequence every time, often returning the same value repeatedly:
# Anti-pattern: usually returns the same value five times
1..5 | ForEach-Object { [System.Random]::new(12345).Next(1,100) }
Instead, create one generator and reuse it across iterations.
Get-Random tips
- Avoid calling
Get-Random -SetSeedin a loop; that reinitializes the generator every time. - Prefer one call that generates all needed values when using
Get-Randomdeterministically.
# Anti-pattern: resets on every call
1..5 | ForEach-Object { Get-Random -SetSeed 42 -Minimum 0 -Maximum 10 }
# Better: one call that emits a deterministic sequence
Get-Random -SetSeed 42 -Minimum 0 -Maximum 10 -Count 5
Practical Recipes: Numbers, Shuffles, Sampling
Reusable, scoped generator
Keep a seeded generator at script or module scope so your functions share the same sequence. You can also surface the seed via parameter or environment variable for easy reproduction.
param([int]$Seed = $(if ($env:RNG_SEED) { [int]$env:RNG_SEED } else { 12345 }))
if (-not $script:Rng) {
$script:Rng = [System.Random]::new($Seed)
}
function Get-DetNumber {
param([int]$Min = 0, [int]$Max = 100)
return $script:Rng.Next($Min, $Max)
}
# Usage
1..5 | ForEach-Object { Get-DetNumber -Min 10 -Max 50 }
Deterministic shuffles (Fisher–Yates)
Sorting by { $rng.Next() } is handy and fine for small lists, but a Fisher–Yates shuffle avoids bias and is O(n). Use the same seed to reproduce the order exactly.
function Shuffle-Deterministic {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[object[]]$InputObject,
[int]$Seed = 0
)
begin {
$rng = [System.Random]::new($Seed)
$buffer = @()
}
process {
$buffer += $InputObject
}
end {
for ($i = $buffer.Length - 1; $i -gt 0; $i--) {
$j = $rng.Next(0, $i + 1)
$tmp = $buffer[$i]
$buffer[$i] = $buffer[$j]
$buffer[$j] = $tmp
}
return $buffer
}
}
# Example
$items = 1..10
$shuffled = $items | Shuffle-Deterministic -Seed 2024
$shuffled
Predictable sampling
To pick a deterministic sample of n items, shuffle with a seed and take the first n.
$population = 'alpha','beta','gamma','delta','epsilon','zeta','eta','theta'
$seed = 31415
$sampleSize = 3
$sample = $population | Shuffle-Deterministic -Seed $seed | Select-Object -First $sampleSize
$sample
Stable train/test splits
For data work, use a seeded shuffle and split by index. Reusing the seed reproduces the split exactly.
$data = 1..100
$seed = 123
$shuf = $data | Shuffle-Deterministic -Seed $seed
$split = [math]::Floor($shuf.Count * 0.8)
$train = $shuf[0..($split-1)]
$test = $shuf[$split..($shuf.Count-1)]
[pscustomobject]@{ TrainCount = $train.Count; TestCount = $test.Count }
DevOps and Testing Workflow: Reproduce, Debug, Ship
Make seed a first-class citizen
- Accept a
-Seedparameter in scripts and functions. - Default to an environment variable (for CI), then a constant fallback.
- Always log the seed so failing runs can be reproduced.
# Seed precedence: CLI > env var > fallback
param([int]$Seed)
if (-not $PSBoundParameters.ContainsKey('Seed')) {
$Seed = if ($env:RNG_SEED -and $env:RNG_SEED -as [int]) { [int]$env:RNG_SEED } else { 2025 }
}
$rng = [System.Random]::new($Seed)
[pscustomobject]@{ Seed = $Seed; Example = $rng.Next(0,100) }
CI/CD runners
- Set
RNG_SEEDin the pipeline to pin behavior. - For nightly variability, use the date as the seed and log it; re-run the same date to reproduce.
- In test jobs, fail with the seed printed so you can reproduce locally.
# GitHub Actions example (env)
# env:
# RNG_SEED: 8675309
# Azure DevOps YAML example
# variables:
# RNG_SEED: 8675309
Safer demos and docs
- Seed your samples so screenshots and blog outputs stay stable.
- Include the seed in your documentation to help readers reproduce exactly.
Security and correctness notes
- Not for secrets:
[System.Random]is not cryptographically secure. Do not use it for keys, tokens, or anything security-sensitive. - Cross-version differences: Exact number sequences from
[System.Random]may differ across .NET versions. For cross-runtime bit-for-bit reproducibility, pin your runtime or ship a custom PRNG with a stable algorithm. For most testing and demo uses, seeding within a consistent runtime is sufficient. - Concurrency:
[System.Random]isn’t thread-safe. In parallel jobs or runspaces, create one generator per worker and seed them deterministically (e.g., base seed + worker index).
Quick reference and gotchas
Do this
- Create one generator:
$rng = [System.Random]::new(Seed). - Pass it where needed or store it in script/module scope.
- Use it for all draws:
$rng.Next(),$rng.Next(min,max),$rng.NextDouble(). - For shuffles, prefer Fisher–Yates.
- Log the seed; accept it via parameter or environment for reproducibility.
Avoid this
- Creating a new
Randominside loops. - Using
Get-Random -SetSeedper iteration. - Expecting the same sequences across different .NET versions and runtimes.
- Using
Randomfor anything security-related.
Complete reproducible snippet
$seed = 12345
$rng = [System.Random]::new($seed)
$nums = 1..5 | ForEach-Object { $rng.Next(1, 100) }
$items = @('alpha','beta','gamma','delta')
$shuffleSeed = 42
$itemsShuffled = $items | Shuffle-Deterministic -Seed $shuffleSeed
[pscustomobject]@{
Seed = $seed
Numbers = ($nums -join ', ')
ShuffleSeed = $shuffleSeed
ShuffledItems = ($itemsShuffled -join ', ')
}
What you get: repeatable tests, stable demos, predictable sampling, and easier debugging. Build confidence with deterministic techniques in PowerShell. For more advanced patterns, see the PowerShell Advanced Cookbook: https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/