Response Caching & Conditional Requests
Layer fine‑grained dynamic caching and validator logic on top of (or independent from) file server level cache headers. This sample demonstrates in‑memory response caching limits plus conditional 304 handling for two dynamic endpoints. See the in-depth HTTP Caching guide for architecture, trade‑offs, and parameter tables.
Prerequisites: see Introduction.
Full source
File: pwsh/tutorial/examples/3.6-Response-Caching.ps1
<#
Sample Kestrun Server demonstrating response caching and conditional requests (ETag / Last-Modified).
Shows how to configure response caching limits and a simple route that returns 304 when unchanged.
FileName: 3.5-Response-Caching.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
New-KrLogger |
Set-KrLoggerLevel -Value Debug |
Add-KrSinkConsole |
Register-KrLogger -Name 'console' -SetAsDefault | Out-Null
# Create a new Kestrun server
New-KrServer -Name "Simple Server"
# Add a listener on configured port and IP
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# Add a file server with browsing enabled use the default Cache-Control headers
Add-KrFileServerMiddleware -RequestPath '/' -RootPath '.\Assets\wwwroot' -EnableDirectoryBrowsing -ContentTypeMap $map
# Add response caching with a 10MB size limit, 64KB max body size, case-sensitive paths,
# a shared max age of 100 seconds, and public cacheability.
Add-KrCacheMiddleware -SizeLimit 10485760 -MaximumBodySize 65536 -UseCaseSensitivePaths -SharedMaxAge 100 -Public -MaxAge 100
# Enable Kestrun configuration
Enable-KrConfiguration
# Map a route that demonstrates response caching with ETag and Last-Modified support
Add-KrMapRoute -Verbs Get -Pattern '/cachetest' -ScriptBlock {
$payload = "This is a cached response." # keep it stable if you want 304s
$etag = '"' + ([Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($payload))).ToLower()) + '"'
# Check for conditional request; if a 304 was sent, just return
if ((Test-KrCacheRevalidation -Payload $payload -ETag $etag -LastModified (Get-Date '2023-01-01'))) {
return
}
# Fresh response
Write-KrTextResponse -InputObject $payload -StatusCode 200
}
# Map another route that demonstrates response caching with ETag and Last-Modified support
# but also adds Cache-Control headers to make it private and must-revalidate (so browsers will revalidate)
# overrides the middleware defaults
Add-KrMapRoute -Verbs Get -Pattern '/custom_cachetest' -ScriptBlock {
Add-KrCacheResponse -Private -MaxAge 120 -MustRevalidate
$payload = "This is a cached response." # keep it stable if you want 304s
$etag = '"' + ([Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($payload))).ToLower()) + '"'
# Check for conditional request; if a 304 was sent, just return
if ((Test-KrCacheRevalidation -Payload $payload -ETag $etag -LastModified (Get-Date '2023-01-01'))) {
Write-KrLog -Level Debug -Message "Returning 304 Not Modified"
return
}
Write-KrLog -Level Debug -Message "Returning 200 OK with payload: {Payload}" -Values $payload
# Fresh response
Write-KrTextResponse -InputObject $payload -StatusCode 200
}
# Start the server asynchronously
Start-KrServer
What this sample shows
- Registering response caching with custom memory + body size constraints
- Using
Test-KrCacheRevalidationto automatically emit304 Not Modified - Emitting a deterministic payload + hash-based ETag (route:
/cachetest) - Overriding cache headers per response with
Add-KrCacheResponse(route:/custom_cachetest) - Combining file server defaults (previous chapter) with dynamic route validators
Step-by-step
- Root + logging: Standard
Initialize-KrRoot+ console logger registration. - Server + listener: Create server; add loopback listener (port 5000).
- File server: Mount root with optional directory browsing (inherits any broad cache directives from previous chapter if you choose).
- Response cache middleware:
Add-KrCacheMiddleware -SizeLimit 10485760 -MaximumBodySize 65536 -UseCaseSensitivePaths -SharedMaxAge 100 -Public -MaxAge 100. /cachetest: Stable payload + computed SHA256 → build explicit ETag, callTest-KrCacheRevalidation; skip body if$true./custom_cachetest: Adds per-response overrides usingAdd-KrCacheResponse -Private -MaxAge 120 -MustRevalidatebefore validator logic.- Start: enable configuration then start server.
How conditional caching works
Test-KrCacheRevalidation:
- Compares supplied / computed ETag vs
If-None-Match(supports weak tags when-Weak). - Compares supplied
-LastModifiedvsIf-Modified-Since. - Writes
304 Not Modified+ validators + cache headers when resource unchanged and returns$true. - Otherwise returns
$false; you write fresh body and (optionally) headers (which may also be cached in-memory subject to size limits).
ETag strategy tips:
- Stable immutable body → hash bytes (cheap, deterministic)
- Frequently changing body → version counter or timestamp string
- Composite data → hash only revision identifiers instead of full concatenated document
Try it
Run the sample; then test both endpoints:
# First request (200 OK + body)
Invoke-WebRequest -Uri 'http://127.0.0.1:5000/cachetest' | Select-Object StatusCode,Headers,@{n='Len';e={$_.Content.Length}}
# Second request likely 304 (short body; ETag stable)
Invoke-WebRequest -Uri 'http://127.0.0.1:5000/cachetest' | Select-Object StatusCode,Headers
# Custom per-response overrides (private + must-revalidate)
Invoke-WebRequest -Uri 'http://127.0.0.1:5000/custom_cachetest' | Select-Object StatusCode,Headers
Invoke-WebRequest -Uri 'http://127.0.0.1:5000/custom_cachetest' -Headers @{ 'If-None-Match' = '"bogusetag"' } | Select-Object StatusCode,Headers
Curl:
curl -i http://127.0.0.1:5000/cachetest
curl -i http://127.0.0.1:5000/custom_cachetest
Middleware tuning parameters
| Parameter | Purpose | Guidance |
|---|---|---|
-SizeLimit | Total bytes across all cached entries | Keep modest; very large caches increase memory pressure |
-MaximumBodySize | Skip caching bodies above this threshold | Set near typical response size; prevents large blobs from evicting many |
-UseCaseSensitivePaths | Distinguish path casing | Useful on Linux; optional on Windows |
-MaxAge / -SharedMaxAge | Freshness lifetimes (private vs shared caches) | Short for rapidly changing content; longer for immutable assets |
-Public / -Private | Cacheability scope | Use -Public only if responses are identical for all users |
Best practices
- Keep payload hashes stable for content that rarely changes to maximize 304 hits.
- Avoid caching highly personalized or security‑sensitive responses.
- Use a versioned path or ETag strategy when deploying updated static bundles (
app.v2.js). - Monitor log volume at Debug/Verbose levels; reduce when not actively diagnosing.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Always 200 responses | ETag not stable / last-mod date shifts | Ensure payload + date inputs are deterministic |
| No 304s with curl | Missing If-None-Match header | Provide it manually (clients add automatically after first call) |
| Large body uncached | Exceeds -MaximumBodySize | Increase threshold if appropriate |
| Memory growth | Cache size too large | Lower -SizeLimit or reduce max age |
References
- Add-KrCacheMiddleware
- Test-KrCacheRevalidation
- Initialize-KrRoot
- Add-KrFileServerMiddleware
- New-KrServer
- Add-KrEndpoint
- Enable-KrConfiguration
- Start-KrServer
Additional examples
Weak ETag (prefix W/ automatically when using -Weak with explicit -ETag in validators):
Add-KrMapRoute -Pattern '/weak' -Verbs GET -ScriptBlock {
$payload = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmm')
if (-not (Test-KrCacheRevalidation -ETag 'v1' -Weak)) {
Write-KrTextResponse $payload
}
}
Last-Modified only (no ETag):
Add-KrMapRoute -Pattern '/modified' -Verbs GET -ScriptBlock {
if (-not (Test-KrCacheRevalidation -Payload 'static-token' -LastModified (Get-Date '2024-01-01'))) {
Write-KrTextResponse 'unchanged since 2024'
}
}
Private + no-store (sensitive ephemeral):
Add-KrMapRoute -Pattern '/secret' -Verbs GET -ScriptBlock {
Add-KrCacheResponse -NoStore -Private
Write-KrTextResponse 'one-time view'
}
Previous / Next
Go back to File Server Caching Headers or continue to Variable Routes.