惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recorded Future
Recorded Future
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
Cyberwarzone
Cyberwarzone
C
CERT Recently Published Vulnerability Notes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
Google Online Security Blog
Google Online Security Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
WordPress大学
WordPress大学
博客园 - 聂微东
L
LINUX DO - 最新话题
月光博客
月光博客
小众软件
小众软件
T
Troy Hunt's Blog
A
Arctic Wolf
量子位
I
Intezer
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
Schneier on Security
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
美团技术团队
The Cloudflare Blog
P
Privacy International News Feed
S
Security Affairs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
N
News | PayPal Newsroom
Apple Machine Learning Research
Apple Machine Learning Research
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 热门话题
V
Vulnerabilities – Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
宝玉的分享
宝玉的分享
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
Hacker News: Ask HN
Hacker News: Ask HN
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Part 9 — Operating the gateway: logs, traces, health, and degraded mode
Akarshan Gan · 2026-05-05 · via DEV Community

The first eight chapters of this series have been about building an Auth Gateway. This one is about living with one.

A gateway in front of every authenticated request is a force multiplier — for both your platform and any oncall page. If something is broken, it's broken everywhere at once. So observability isn't a Chapter 9 thing. It's a Chapter 0 thing. We just describe it last because there's enough mechanism to talk about that you need the rest of the series first.

This chapter covers the four things you need to be able to do at 3 AM:

  1. Read a single log line and understand what happened.
  2. Trace a slow request from edge to upstream.
  3. Tell whether a pod is alive, ready, or in deep trouble.
  4. Get an alert once — not once per pod per second — when something degrades.

The log line

There are exactly two structured log lines per protected request: one from NGINX, one from the Auth Service. They share request_id, so you can join them.

The NGINX line

Every request is logged as JSON to stdout via NGINX's log_format main:

log_format main escape=json '{
  "logType":"NGINX_LOGS",
  "request_id":"$request_id",
  "time_local":"$time_iso8601",
  "remote_addr":"$remote_addr",
  "request_method":"$request_method",
  "request_uri":"$request_uri",
  "request_path":"$uri",
  "slug":"$location_path",
  "product":"$product",
  "microservice":"$microservice",
  "status":"$status",
  "status_class":"$status_class",
  "request_time_ms":"$request_time_millis",
  "service_request_time_ms":"$upstream_response_time_millis",
  "service_connect_time":"$upstream_connect_time",
  "auth_request_time_ms":"$auth_time_millis",
  "auth_connect_time":"$auth_connect_time",
  "body_bytes_sent":"$body_bytes_sent",
  "http_referer":"$http_referer",
  "http_user_agent":"$http_user_agent",
  "http_x_forwarded_for":"$http_x_forwarded_for",
  "http_host":"$http_host",
  "tenant_id":"$tenant_id",
  "tenant_namespace":"$tenant_namespace",
  "identity_id":"$identity_id",
  "identity_type":"$identity_type",
  "auth_error_message":"$auth_error_message",
  "auth_error_code":"$auth_error_code"
}';

Enter fullscreen mode Exit fullscreen mode

A few fields that matter more than they look:

  • auth_request_time_ms — how long the auth subrequest took. We graph this. We page on the p99 going above 50 ms.
  • service_request_time_ms — how long the upstream took, excluding the auth subrequest. Sequential, not overlapping (Chapter 2).
  • status_class1xx/2xx/3xx/4xx/5xx. Faster than parsing status for dashboard breakdowns.
  • tenant_id — the resolved tenant. Always grep by tenant first.
  • auth_error_code + auth_error_message — populated on deny. Empty on allow.

Two extra log formats for the fail paths

When NGINX hits @auth_unavailable or @upstream_unavailable, we log to a different format:

log_format auth_unavailable     '...auth-specific fields...';
log_format upstream_unavailable '...upstream-specific fields...';

location @auth_unavailable {
  internal;
  access_log /dev/stdout auth_unavailable;
  return 503 ...;
}

Enter fullscreen mode Exit fullscreen mode

The reason: when something is on fire, you want it isolated in its own log stream. Dashboards built off main get drowned by 200s; a dashboard against auth_unavailable shows you exactly the broken bucket without filtering.

The Auth Service line

The Auth Service emits exactly one AUTH_DECISION log per request:

{
  "level":"info",
  "ts":"2026-05-01T12:34:56.789Z",
  "logger":"AUTH_DECISION",
  "request_id":"550e8400-e29b-41d4-a716-446655440000",
  "trace_id":"4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id":"00f067aa0ba902b7",
  "uri":"/api/v1/users",
  "method":"GET",
  "slug":"user-service",
  "tenant_id":"mt_prod",
  "endpoint_type":"ACCESS_CONTROLLED",
  "identity_type":"USER",
  "identity_id":"user@example.com",
  "auth_method":"bearer_token",
  "decision_reason":"ACCESS_LEVEL_MATCH",
  "outcome":"allow",
  "status":200,
  "duration_ms":2.5,
  "authn_ms":1.8,
  "authz_ms":0.7,
  "jwt_cache_hit":true,
  "bitmap_authz_used":true,
  "granted_access_levels":["product:admin"],
  "token_revoked":false,
  "slow":false
}

Enter fullscreen mode Exit fullscreen mode

This is the single most important artifact in the whole gateway. It is, in dry terms, our auth audit log. In practical terms it's the thing oncall greps when anything goes weird.

Design rules we apply to it religiously:

  • Exactly one line per request. No "starting auth", "authenticated", "authorizing", "decided" — those make the storyline split across N entries that you have to stitch back together. One line, one decision, every field.
  • Every field, every time. If a field doesn't apply (e.g., bitmap_authz_used for an OPEN endpoint), it's false or empty, not omitted. Optional fields make ad-hoc queries painful.
  • decision_reason is enum-only. Free-form strings here would be the death of dashboards. New reasons require code review (Chapter 3).
  • Trace IDs are present. trace_id and span_id are pulled from the OpenTelemetry context, so the log line stitches to traces in our backend without join logic.

Joining NGINX and Auth Service

request_id is generated by NGINX ($request_id) and forwarded to the Auth Service via the subrequest header X-Request-ID. Both log lines carry it. A typical investigation:

1. user reports 401 at 12:34:56 UTC
2. grep tenant_id="mt_prod" identity_id="..." in NGINX logs around the time
3. capture request_id
4. grep that request_id in Auth Service logs
5. read decision_reason — full story

Enter fullscreen mode Exit fullscreen mode

The whole graph of "client → ingress → NGINX → Auth Service → upstream" stitches back together by request_id.

Tracing

OpenTelemetry instrumentation runs across both NGINX (via the otel module if compiled in; we're tracking that as a future improvement) and the Auth Service. The Auth Service span looks like:

SPAN: pth-auth-service POST /auth
├── ATTRIBUTES
│   ├── http.route = "/auth"
│   ├── auth.tenant_id = "mt_prod"
│   ├── auth.endpoint_type = "ACCESS_CONTROLLED"
│   ├── auth.outcome = "allow"
│   └── auth.decision_reason = "ACCESS_LEVEL_MATCH"
├── EVENTS
│   ├── jwt.cache.hit
│   ├── route.cache.hit
│   └── bitmap.match
└── DURATION 2.5ms

Enter fullscreen mode Exit fullscreen mode

The trace context propagates to upstream services via standard W3C trace headers. So a single trace shows: client → ingress → NGINX (eventually, via otel module) → Auth Service span → upstream service span(s) → DB calls inside the upstream. The whole story.

We don't turn on full sampling. 1% sampling at edges, 100% sampling for spans tagged auth.outcome="deny". The deny path is where the interesting investigations happen; sampling it fully gives us forensic detail without exploding storage.

Health probes

Three K8s probe endpoints, each with a different purpose.

/livez — process is alive

func Liveness(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

Enter fullscreen mode Exit fullscreen mode

Returns 200, always. The contract: as long as this handler runs, the process isn't deadlocked. K8s only kills the pod if the request times out (handler doesn't run at all).

What /livez does not check:

  • It does not check the trie.
  • It does not check Redis.
  • It does not check Postgres.

This is intentional. A pod whose Redis connection died can still serve cache-hot requests correctly. Killing it on a Redis outage is exactly the wrong thing to do — you turn a cache-hit-100%-but-Redis-down state into a cluster-wide rolling restart.

/readyz — pod can take traffic

func Readiness(c *gin.Context) {
    if !trieLoaded() {
        c.JSON(503, gin.H{"status": "trie not loaded"})
        return
    }
    if revocationExpected() && !revocationServiceReady() {
        c.JSON(503, gin.H{"status": "revocation cache not ready"})
        return
    }
    c.JSON(200, gin.H{"status": "ok"})
}

Enter fullscreen mode Exit fullscreen mode

Two gates:

  1. Trie loaded. Without it, we can't classify any endpoint. The pod is useless until the trie is in memory.
  2. Revocation cache ready (if revocation is enabled). Without it, fail-closed designs would deny everything; fail-open designs would miss every revocation. Either way, not ready.

Notably not gated on Redis health: a pod that lost Redis after readiness flips green stays ready. Refreshes fail loudly via Slack, but live traffic isn't disrupted.

/healthz — deep health

func Health(c *gin.Context) {
    pgErr := pingPostgres(c.Request.Context())
    rdErr := pingRedis(c.Request.Context())
    if pgErr != nil || rdErr != nil {
        c.JSON(503, gin.H{
            "status": "degraded",
            "postgres": errString(pgErr),
            "redis":    errString(rdErr),
        })
        return
    }
    c.JSON(200, gin.H{"status": "ok"})
}

Enter fullscreen mode Exit fullscreen mode

This one does depend on Redis and Postgres. It's not used by Kubernetes — it's used by external monitoring (Pingdom, status pages). The distinction matters:

  • K8s probes determine traffic routing. They should be tolerant — every false-positive failure pulls a pod out of service.
  • Monitoring probes determine alerting. They should be strict — they tell humans something is wrong, not the load balancer.

A common mistake is wiring /healthz to readinessProbe. Don't. You will pull pods out of the rotation on a transient Redis blip and convert a degraded state into an outage.

stateDiagram-v2
    [*] --> Booting
    Booting --> NotReady: trie empty
    NotReady --> Ready: trie loaded AND<br/>(revocation disabled OR revocation cache warm)
    Ready --> Degraded: Redis stream XREAD failure
    Degraded --> Ready: reconnect
    Ready --> Live: /livez always 200
    Ready --> DeepCheck: /healthz pings PG + Redis

Enter fullscreen mode Exit fullscreen mode

Degraded mode

The gateway is built to tolerate three specific kinds of trouble:

Redis is slow or down

  • Revocation stream consumer fails. streamDegraded atomic flips true. Slack alert fires once. Hot path keeps working — local cache is in memory.
  • Pub/Sub subscriber reconnects in the background. On reconnect, the subscriber re-subscribes. The periodic cleanup goroutine resyncs the ZSET when it next runs.
  • SA version sync fails. Local SA version map is unchanged. Tokens continue to validate against the last known versions until the next successful sync. Slack alert fires.

Postgres is slow or down

  • Trie reload fails. Existing trie remains in memory. Slack alert. Hot path is unaffected.
  • New pods cannot start (initial trie load blocks readiness). Existing pods serve.
  • This is a partial outage: scaling up is broken, current capacity still works.

Slack is slow or down

  • Alerts are fire-and-forget goroutines. We don't block the hot path on Slack.
  • If Slack is down, alerts are queued in goroutines for a configured timeout (5s) then dropped. We don't retry forever and OOM the pod.

The alert tree

flowchart TD
    Boot[startup] --> A1{revocation Redis OK?}
    A1 -->|no| Alert1[Slack: TokenRevocationService Redis client not initialized<br/>readyz=503]
    Run[runtime] --> A2{XREAD error?}
    A2 -->|yes| Alert2[Slack: stream degraded once]
    A2 -->|recover| Alert3[Slack: stream recovered]
    Run --> A3{localCache size > MAX?}
    A3 -->|yes| Alert4[Slack: cache overflow once<br/>fall back to ZSCORE]
    Run --> A4{RSA key missing for tenant?}
    A4 -->|yes| Alert5[Slack: per-tenant dedup]

Enter fullscreen mode Exit fullscreen mode

The Slack-alerter pattern

Three rules we apply rigorously to alert code, because we learned them the hard way:

Rule 1: Atomic-bool dedup per state transition

type degradeFlag struct {
    on atomic.Bool
}

func (d *degradeFlag) MarkDegraded(reason string) {
    if !d.on.Swap(true) {
        slack.Alert("auth.degraded", reason)
    }
}

func (d *degradeFlag) MarkRecovered() {
    if d.on.Swap(false) {
        slack.Alert("auth.recovered", "")
    }
}

Enter fullscreen mode Exit fullscreen mode

The Swap returns the previous value. If it was already true, we don't re-alert. We alert once on the transition, and once on the recovery.

Without this, a Redis outage produces thousands of Slack messages per pod per minute. The first time it happened, oncall threw their phone across the room.

Rule 2: Per-(pod × cause) dedup, not per-cause

A 100-pod deployment hitting the same RSA key misconfiguration alerts 100 times. That's correct: each pod is a separate runtime, each could have its own state, each is a separate alert source. We tag every alert with the pod's hostname so you can see in Slack whether it's a single-pod or fleet-wide problem.

Rule 3: Deployment-tag prefix

Every alert is prefixed with [customer-env]:

[acme-prod] auth.degraded TokenRevocationService Redis stream degraded

Enter fullscreen mode Exit fullscreen mode

This makes a single Slack channel viable for many environments. Without the prefix, you can't tell at a glance whether the alert is from staging or prod.

What we don't alert on

  • Individual auth failures. Login throttling, expired tokens, denied requests — those are normal and high-volume. They're in dashboards, not Slack.
  • High latency on a single request. Latency alerts go on rolling p99, not on individual outliers.
  • Anything below "the gateway behaves correctly but degraded." If the system self-heals quietly, we don't page humans.

NGINX-specific operations

A few NGINX-isms worth calling out.

Graceful shutdown

The chart's deployment does this on preStop:

lifecycle:
  preStop:
    exec:
      command:
        - /bin/sh
        - -c
        - |
          echo "[preStop] draining 15 seconds..."
          sleep 15
          echo "[preStop] nginx -s quit..."
          nginx -s quit
          while pgrep -x nginx > /dev/null; do sleep 1; done

Enter fullscreen mode Exit fullscreen mode

15 seconds of drain, then nginx -s quit (graceful — drain in-flight requests, then exit), then wait for all worker processes to finish. Combined with terminationGracePeriodSeconds: 60, we have ~60 seconds total budget for clean shutdown. Without this, rolling deploys produced visible spikes of 502s in client logs.

Worker tuning

worker_processes  auto;
worker_rlimit_nofile 65535;
worker_shutdown_timeout 30s;

events {
  worker_connections 10240;
  use epoll;
  multi_accept on;
  accept_mutex off;
}

Enter fullscreen mode Exit fullscreen mode

worker_processes auto scales to CPU count. accept_mutex off is the modern default — let kernel epoll handle accept distribution. worker_connections 10240 is per worker, so a 4-core pod handles ~40k concurrent connections.

Upstream keepalive

upstream auth_service {
  server auth-service-golang.<ns>.svc.cluster.local:80
         max_fails=3 fail_timeout=30s;
  keepalive 64;
  keepalive_requests 1000;
  keepalive_timeout 60s;
}

Enter fullscreen mode Exit fullscreen mode

keepalive 64 is per worker. A 4-worker NGINX with 64 keepalive maintains 256 keepalive connections to the Auth Service. Without keepalive, every subrequest opens a fresh TCP connection — fatal at any real RPS. The first time we deployed without it, p99 auth time was 80 ms. With it: 3 ms.

max_fails=3 fail_timeout=30s marks the upstream "down" after 3 consecutive failures and stops sending traffic for 30 seconds. Combined with the retry config in auth.conf, this gives smooth failover when one auth pod is sick.

A picture of where the time goes

gantt
    title Single request as seen by NGINX
    dateFormat  X
    axisFormat %s ms
    section NGINX
    receive + route match : 0, 1
    auth subrequest       : 1, 5
    proxy upstream        : 6, 18
    log JSON              : 24, 1

Enter fullscreen mode Exit fullscreen mode

The auth subrequest is the smallest thing on the timeline. That's not by accident. Chapter 8's caches are the reason it stays small. Chapter 9 is what tells you when it stops staying small.

What's next

Chapter 10 is the retrospective: what we'd build differently if we did it from scratch, what tools we wish we'd added on day one, and the maturity progression from "auth library in every service" to "production-grade gateway." It's the chapter you write after operating the thing for two years, and it's the one I'd want to read if I were starting from scratch.