{"slug":"cloudflare-pingora-architecture-diagram","url":"https://tako.sh/blog/cloudflare-pingora-architecture-diagram/","canonical":"https://tako.sh/blog/cloudflare-pingora-architecture-diagram/","title":"Cloudflare Pingora Architecture Diagram: How Tako Routes Requests, TLS, and Cold Starts","date":"2026-05-21T13:31","description":"A request-path diagram of Tako's Pingora proxy: SNI, route matching, static assets, cold starts, load balancing, and upstream proxying.","author":null,"image":"674ebe7d7a9b","imageAlt":null,"headings":[{"depth":2,"slug":"the-request-path-in-one-diagram","text":"The request path in one diagram"},{"depth":2,"slug":"tls-happens-before-app-routing","text":"TLS happens before app routing"},{"depth":2,"slug":"route-matching-is-pure-host-and-path","text":"Route matching is pure host and path"},{"depth":2,"slug":"cold-starts-are-backend-resolution","text":"Cold starts are backend resolution"},{"depth":2,"slug":"upstream-proxying-is-the-last-step","text":"Upstream proxying is the last step"},{"depth":2,"slug":"why-this-architecture-matters","text":"Why this architecture matters"}],"markdown":"Pingora is not a config file with a proxy hiding behind it. It is a Rust framework for building programmable network services, which is exactly why we use it in Tako.\n\nIn [our Pingora vs Caddy vs Traefik post](/blog/pingora-vs-caddy-vs-traefik/), we talked about the decision. This post is the wiring diagram: what actually happens when a browser request hits a Tako server, how TLS is selected, where route matching happens, when the proxy serves a file directly, and how a scaled-to-zero app wakes up before the request is forwarded.\n\nIf you came here looking for a Cloudflare Pingora architecture diagram, this is not Cloudflare's internal edge. This is Tako's edge path, built on the same programmable proxy framework.\n\n## The request path in one diagram\n\nPingora's [`ProxyHttp` lifecycle](https://github.com/cloudflare/pingora/blob/main/docs/user_guide/internals.md) is built around hooks: inspect the request, choose an upstream peer, adjust the upstream request, observe the response, and log the result. Tako uses those hooks as the edge control plane for deployed apps.\n\n```d2\ndirection: right\n\nbrowser: Browser {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nedge: \"Pingora listener\\n:80 / :443\" {\n  style.fill: \"#9BC4B6\"\n}\n\ntls: \"TLS + SNI\\ncertificate lookup\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nrequest_filter: \"request_filter\\nhost + path\" {\n  style.fill: \"#9BC4B6\"\n}\n\nroutes: \"Tako route table\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nfast_path: \"Tako-owned endpoints\\nand static assets\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nbackend: \"backend resolution\" {\n  style.fill: \"#9BC4B6\"\n}\n\ncold: \"cold start gate\\nif scaled to zero\" {\n  style.fill: \"#E88783\"\n}\n\nlb: \"round-robin\\nhealthy instance\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nupstream: \"upstream_peer\\n127.0.0.1:port\" {\n  style.fill: \"#9BC4B6\"\n}\n\napp: \"app process\\nSDK ready on fd 4\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nbrowser -> edge: \"HTTP request\"\nedge -> tls: \"HTTPS handshake\"\ntls -> request_filter: \"request headers\"\nrequest_filter -> routes: \"match host + path\"\nroutes -> fast_path: \"/_tako/* or file asset\"\nfast_path -> browser: \"direct response\"\nroutes -> backend: \"dynamic request\"\nbackend -> cold: \"no healthy instance\"\ncold -> app: \"spawn + wait\"\napp -> lb: \"healthy\"\nbackend -> lb: \"ready backend\"\nlb -> upstream: \"selected endpoint\"\nupstream -> app: \"proxied HTTP\"\napp -> browser: \"response\"\n```\n\nThe important part is where the decisions live. Tako does not generate an Nginx config, reload an external proxy, and hope the process manager agrees. The Pingora proxy, route table, load balancer, TLS manager, static file path, cold-start manager, and app process state all live in the same server process.\n\nThat is why features like [zero-downtime deploys](/blog/zero-downtime-deploys-without-a-container-in-sight/), [scale-to-zero](/docs/deployment/), and [multiple apps on one VPS](/blog/how-to-host-multiple-apps-on-one-vps-with-automatic-https/) are not separate layers. They are all request-path decisions.\n\n## TLS happens before app routing\n\nThe first choice is not \"which app gets this request?\" It is \"which certificate should the listener present?\"\n\nFor HTTPS, the browser sends SNI during the TLS handshake. Tako's TLS layer asks the certificate manager for an exact hostname match first, then tries a wildcard certificate match, then falls back to a default self-signed certificate when no matching certificate exists yet. That fallback lets the TLS handshake complete so the HTTP layer can return a normal status like `404` for an unknown host.\n\nCertificate behavior is tied to routes in [`tako.toml`](/docs/tako-toml/):\n\n| Route shape          | Certificate behavior                            |\n| -------------------- | ----------------------------------------------- |\n| `example.com`        | ACME HTTP-01 certificate for the exact hostname |\n| `api.example.com`    | ACME HTTP-01 certificate for the exact hostname |\n| `example.com/docs/*` | Uses the `example.com` certificate              |\n| `*.example.com`      | Wildcard certificate through Cloudflare DNS-01  |\n\nHTTP requests normally redirect to HTTPS with a `307`, except for `/.well-known/acme-challenge/*`, which the proxy handles directly so Let's Encrypt can verify the domain. Wildcard routes are the special case: they require Cloudflare DNS-01 credentials because HTTP-01 cannot prove control of every possible subdomain. The deploy flow checks that before shipping the app.\n\nSo by the time Tako starts thinking about apps, TLS is already settled. The request is inside Pingora's HTTP lifecycle with a host, path, headers, and a per-request context.\n\n## Route matching is pure host and path\n\nTako's route table is intentionally simple. Deployed apps contribute environment-level routes, and incoming requests are matched by hostname and path. The most specific match wins: exact host beats wildcard host, and longer path beats shorter path.\n\n```toml\nname = \"docs\"\nruntime = \"bun\"\n\n[envs.production]\nroutes = [\n  \"example.com/docs/*\",\n  \"docs.example.com\"\n]\nservers = [\"prod\"]\n```\n\nThe route table accepts four useful shapes:\n\n| Route                   | What it matches                    |\n| ----------------------- | ---------------------------------- |\n| `api.example.com`       | Exact hostname                     |\n| `*.example.com`         | Any one matching subdomain         |\n| `example.com/api/*`     | Hostname plus path prefix          |\n| `*.example.com/admin/*` | Wildcard hostname plus path prefix |\n\nOnce `request_filter` has the selected app, it handles edge-owned responses before forwarding to an app process.\n\nFirst, `/_tako/*` is reserved for Tako's public endpoints. Durable channels, image optimization, and signed storage URLs live there. Those requests are not ordinary app routes.\n\nSecond, static assets get a fast path. If a request looks like a file, Tako checks the deployed app's `public/` directory and serves the file directly when present. For path-prefixed routes, it also tries the prefix-stripped asset path. That is why an app mounted at `example.com/docs/*` can still serve a built asset like `/assets/main.js` when the browser asks for `/docs/assets/main.js`.\n\nOnly after those checks does the proxy need a backend process.\n\n## Cold starts are backend resolution\n\nBackend resolution asks a narrow question: is there a healthy instance for this app?\n\nIf yes, Tako selects one. If not, the answer depends on the app's desired instance count. For an always-on app, no healthy backend is an outage, so production returns a generic `503 Service Unavailable` and logs the app-scoped diagnostic. For an app that has been scaled to zero, no healthy backend is the wake-up path.\n\nThe cold-start manager uses a leader/waiter pattern:\n\n| Situation                                  | Proxy behavior                              |\n| ------------------------------------------ | ------------------------------------------- |\n| First request arrives while app is at zero | Becomes the leader and starts one instance  |\n| More requests arrive during startup        | Wait behind the same cold start             |\n| Startup succeeds                           | Waiters continue to the new healthy backend |\n| Startup exceeds 30 seconds                 | Return `504 Gateway Timeout`                |\n| Spawn or readiness fails                   | Return `502 Bad Gateway`                    |\n| More than 1000 requests wait               | Return `503 Service Unavailable`            |\n\nThe app process itself is still a normal native process. Tako sets `HOST=127.0.0.1` and `PORT=0`, then the [SDK](/docs/) binds an OS-assigned port and reports it back over file descriptor 4. The server probes the SDK status endpoint, marks the instance healthy, records cold-start metrics, and releases the waiting requests.\n\nThere is no container image to unpack and no external proxy config to rewrite. The request that discovered the app was cold is the same request that waits for the instance to become routable.\n\n## Upstream proxying is the last step\n\nOnce a backend is ready, Tako's load balancer chooses a healthy instance with round-robin selection. The selected instance has a loopback endpoint like `127.0.0.1:47831`, and Pingora's `upstream_peer` hook turns that into the actual upstream connection.\n\nBefore forwarding, Tako adjusts the upstream request:\n\n| Header or field         | What Tako does                                        |\n| ----------------------- | ----------------------------------------------------- |\n| `X-Forwarded-Proto`     | Sets `https` or `http` based on the effective request |\n| `X-Forwarded-For`       | Sets the resolved client IP when trusted              |\n| `Forwarded`             | Removes it before proxying                            |\n| `X-Tako-Internal-Token` | Removes client-supplied values                        |\n| Request body            | Enforces the configured body-size limit               |\n\nWhen response headers arrive, Tako records upstream timing. When the request finishes, it releases per-IP rate-limit accounting, marks the selected instance request as finished, records end-to-end request metrics, and logs the final status.\n\nThat is the full shape: SNI first, route table second, Tako-owned endpoints and static assets before app traffic, cold-start backend resolution when needed, then loopback proxying to a healthy native process.\n\n## Why this architecture matters\n\nA standalone reverse proxy is great when routing is the whole job. Tako's proxy has a different job. It needs to know whether a deploy is rolling out, whether an instance is healthy, whether a route belongs to a static asset, whether a request should wake a sleeping app, and whether a wildcard hostname needs DNS-01 certificate handling.\n\nPingora gives us the programmable request lifecycle for that. Tako supplies the app model around it: [`tako.toml`](/docs/tako-toml/), [`tako deploy`](/docs/deployment/), local HTTPS in [`tako dev`](/docs/development/), encrypted secrets, runtime readiness, workflows, channels, and image optimization.\n\nThe result is a small edge inside your VPS: one Rust server process that terminates TLS, routes requests, manages app instances, and keeps the proxy aware of the deployment state it is serving."}