Skip to content

Instantly share code, notes, and snippets.

@allan-gar2x
Created March 31, 2026 11:38
Show Gist options
  • Select an option

  • Save allan-gar2x/309257c8e0bb8e76cfbb91d524aa628a to your computer and use it in GitHub Desktop.

Select an option

Save allan-gar2x/309257c8e0bb8e76cfbb91d524aa628a to your computer and use it in GitHub Desktop.
WAF CloudWatch Logs Insights — Caching Strategy & Cost Analysis

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.

Expensive Endpoints

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.


Cost Analysis

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

Cache Key Design

waf-cache#{stage}#{queryType}#{range}
e.g. waf-cache#dev#logs-insights#24h
     waf-cache#production#blocked-requests#7d
     waf-cache#dev#traffic#1h

Proposed TTLs

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

Implementation Plan

Phase 0 — Prerequisites

  • 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

Files to Modify

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment