WAF CloudWatch Logs Insights — Caching Strategy & Cost Analysis
Project: lumina5 / bike4mind
Date: 2026-03-31
Branch: feat(secops)/waf-updates-cloudwatch
Current Architecture (No Server-Side Cache)
Every admin page load triggers 3–4 CloudWatch Logs Insights queries , each polling up to 60 seconds . Additionally, CloudFront + WAFv2 discovery calls fire on every single request.
Endpoint
AWS Services Called
Queries per Request
GET /api/admin/security-dashboard/waf-logs-insights
CloudFront + WAFv2 + CloudWatch Logs Insights
3 parallel CW queries (Top URIs, Top IPs, Rate Limit)
GET /api/admin/security-dashboard/waf-blocked-requests
CloudFront + WAFv2 + CloudWatch Logs Insights
1 CW query (raw BLOCK logs, limit 1000)
GET /api/admin/security-dashboard/waf-traffic
CloudFront + CloudWatch Metrics + WAFv2 ListMetrics
1 ListMetrics + 1 GetMetricData (10+ expressions)
CloudWatch Logs Insights Queries Running Today
-- 1. Top Blocked URIs
fields httpRequest .uri as uri
| filter action = " BLOCK" and ispresent(uri)
| stats count () as requests by uri
| sort requests desc | limit 10
-- 2. Top Client IPs with Country
fields httpRequest .clientIp as clientIp, httpRequest .country as country
| filter ispresent(clientIp)
| stats count () as requests by clientIp, country
| sort requests desc | limit 10
-- 3. Rate Limit Usage (5-min bins)
fields httpRequest .clientIp as ip, httpRequest .uri as uri
| filter ispresent(ip) and ispresent(uri)
| stats count () as requests by ip, uri, bin(5min)
| stats max (requests) as peakRequests by ip, uri
| sort peakRequests desc | limit 10
-- 4. Blocked Requests (raw logs)
fields @timestamp , @message
| filter action = " BLOCK"
| sort @timestamp desc | limit 1000
Existing Client-Side Caching (React Query)
Hook
staleTime
Scope
useSecurityDashboardWafLogsInsights()
1 minute
Per browser tab
useSecurityDashboardWafBlockedRequests()
1 minute
Per browser tab
useSecurityDashboardWafTraffic()
1 minute
Per browser tab
⚠️ Client-side only — 5 admins with separate browser tabs = 5 independent CloudWatch query sets firing independently.
AWS CloudWatch Logs Insights Pricing
$0.005 per GB scanned
Queries scan the entire log group for the selected time range
A 7-day range on a moderately busy app = 5–20 GB per query
Scenario: 5 Admins, 10 Dashboard Opens Each per Day
Range
GB per Query
Queries/Day (before cache)
Daily Cost
Monthly Cost
1h
~0.2 GB
5 × 10 × 4 = 200
$0.20
$6
24h
~2 GB
200
$2.00
$60
7d
~10 GB
200
$10.00
$300
With Server-Side Cache (10-min TTL)
Cache reduces 200 queries/day → ~14 (one per TTL window per range):
Range
Queries/Day (after cache)
Daily Cost
Monthly Cost
Monthly Savings
1h
~14
$0.014
$0.42
~$5.60
24h
~14
$0.14
$4.20
~$55.80
7d
~14
$0.70
$21.00
~$279
💡 At heavy admin usage (20 admins, 20 opens/day, mixed ranges): $400–$600/month savings
Where to Cache — Options Compared
Option
Pros
Cons
Verdict
Browser (React Query only)
Already exists, zero infra
Per-user only — N admins = N independent query sets
❌ Not enough
MongoDB
Already connected, zero new infra
Not designed for volatile cache; TTL index overhead; adds DB load
❌ Wrong tool
Redis / ElastiCache
Purpose-built, fast, TTL native
New infra, VPC config, ~$15–50/month base cost
⚠️ Overkill for now
Lambda in-memory (module-scope Map)
Zero infra, zero cost, already proven (secretCache.ts)
Resets on cold start, not shared across Lambda instances
✅ Best starting point
DynamoDB TTL table
Shared across all Lambda instances, serverless, auto-expiry
Per-read/write cost (negligible)
✅ Best for multi-instance
Recommended Strategy: Two-Layer Cache
Layer 1 — Lambda In-Memory (same pattern as secretCache.ts)
Deduplicates concurrent requests on the same Lambda instance
Zero cost, zero latency overhead
Already proven in codebase: /apps/client/server/security/secretCache.ts
// Pattern already used in codebase
class WafQueryCache {
private cache = new Map < string , { value : unknown ; expiresAt : number } > ( ) ;
async getOrFetch < T > ( key : string , ttlMs : number , fetcher : ( ) => Promise < T > ) : Promise < T > {
const hit = this . cache . get ( key ) ;
if ( hit && hit . expiresAt > Date . now ( ) ) return hit . value as T ;
const value = await fetcher ( ) ;
this . cache . set ( key , { value, expiresAt : Date . now ( ) + ttlMs } ) ;
return value ;
}
}
Layer 2 — DynamoDB TTL Table (shared across all Lambda instances)
Shared cache survives Lambda restarts and serves all concurrent instances
Write on cache miss → read on cache hit
DynamoDB auto-deletes expired items via TTL attribute
Cost: ~$0.00065/write, ~$0.00013/read — negligible vs CloudWatch savings
waf-cache#{stage}#{queryType}#{range}
e.g. waf-cache#dev#logs-insights#24h
waf-cache#production#blocked-requests#7d
waf-cache#dev#traffic#1h
Query Type
Proposed TTL
Rationale
Logs Insights (top URIs, IPs, rate limit)
10 minutes
Slow-changing, expensive to recompute
Blocked Requests (raw log list)
5 minutes
Higher freshness value for ops use
WAF Traffic Metrics
5 minutes
CloudWatch metrics lag 1–3 min anyway
CloudFront → WebACL resolution
30 minutes
Infrastructure config, rarely changes
Add DynamoDB cache table to infra/dynamodb.ts
Create apps/client/server/security/wafQueryCache.ts utility
Phase 1 — Wrap Expensive Queries
getWafLogsInsightsOverview() → cache result keyed by stage + range
getWafBlockedRequests() → cache result keyed by stage + range
getWafTrafficOverview() → cache result keyed by stage + range + period + includeRules
CloudFront → WebACL → log group resolution → cache separately (30 min TTL)
Phase 2 — Align React Query staleTime
Increase client staleTime from 1 min → 5 min for logs insights hooks
No point refetching from browser if server cache is still warm
Phase 3 — Cache Invalidation
Add ?bust=1 query param support to force bypass (for manual refresh button)
Cache key includes all query parameters — switching ranges always fetches fresh
File
Change
infra/dynamodb.ts
Add WafQueryCacheTable DynamoDB table with TTL
infra/web.ts
Link new table to frontend Lambda
apps/client/server/security/wafQueryCache.ts
New — two-layer cache utility
apps/client/server/security/wafLogsInsights.ts
Wrap getWafLogsInsightsOverview() and getWafBlockedRequests()
apps/client/server/security/wafTraffic.ts
Wrap getWafTrafficOverview()
apps/client/app/hooks/data/admin.ts
Increase staleTime for 3 WAF hooks from 1 min → 5 min
Analysis generated from codebase audit of lumina5 on 2026-03-31.