The FastCGI specification is 30 years old today. It was published on April 29, 1996. It is, on a couple of structural axes that have come to matter a great deal in 2026, the protocol that the rest of the industry should be using for proxy-to-backend communication, instead of the protocol it actually uses, which is HTTP. This is not my argument; it is Andrew Ayer's argument, made on the 30th-birthday post on his blog at agwa.name. Ayer is the founder of SSLMate and notes the company has run FastCGI in production for over ten years. He has, in other words, the receipts.
The piece is short and operational, and the HN thread on it ran 100 comments deep — modest by 2026 front-page standards but heavy on long-tenure-operator testimony. The piece argues that HTTP-as-reverse-proxy-protocol has two structural failure modes that FastCGI does not have, and that the industry's continued reliance on HTTP for this specific use case is the result of accreted preference rather than careful comparison. The thread argues, additionally, that the reason HTTP won despite being worse is itself worth understanding — and that the answer is the end-to-end principle, applied where it should not have been.
The two structural failure modes
The first failure mode is desync attacks, also known as request smuggling. HTTP/1.1 has the property of looking simple on the surface — "it's just text!" in Ayer's parenthetical — and being a nightmare to parse correctly. There is no explicit framing in the protocol. The message itself describes where it ends, via Content-Length, Transfer-Encoding, or sometimes both, and there are enough edge cases in the parsing rules that two HTTP implementations can disagree about where one message ends and the next begins. When the implementation that disagrees with its neighbor is your reverse proxy and the implementation it disagrees with is your backend, an attacker can smuggle a second request inside the body of a first, and the second request gets handled with the privileges of the wrong session.
The recent example Ayer leads with is a desync vulnerability in Discord's media proxy, disclosed earlier this year, that allowed spying on private attachments. The class is decades old. Watchfire described it in 2005 and warned, prophetically, that the parser-divergence approach to fixing it would be a losing strategy. James Kettle at PortSwigger has spent the last several years finding new variants on a roughly annual cadence. After the most recent batch he declared, on a single-purpose website with the URL http1mustdie.com, that "HTTP/1.1 must die."
HTTP/2 fixes desync, when consistently used between proxy and backend, by putting clear boundaries around messages. FastCGI fixed it in 1996, with a simpler protocol, by also putting clear boundaries around messages. Ayer notes — and this is the part that lands — that nginx has supported FastCGI backends since its first release, but only got HTTP/2 backend support in late 2025. Apache's HTTP/2 backend support is, as of this writing, still experimental. FastCGI has been there the entire time.
The second failure mode is untrusted-header confusion. HTTP has no clean way for the proxy to convey trusted information about a request — the real client IP, the authenticated username, client-certificate details — separately from the headers the client itself sent. The conventional approach is to use additional HTTP headers (X-Real-IP, X-Forwarded-For, True-Client-IP) and trust the proxy to strip any client-supplied headers with those names before adding its own. In theory this works. In practice it is a minefield, because there is no structural distinction between trusted and untrusted headers — it's all just headers — and any part of your stack that looks at the wrong header without your knowledge can be tricked. Ayer's specific example: Go's Chi middleware reads True-Client-IP first, falling back to X-Real-IP. Even if your proxy correctly strips X-Real-IP, an attacker who sends True-Client-IP defeats it.
FastCGI structurally cannot have this bug. Trusted parameters and HTTP headers travel in the same key/value list, but HTTP headers are prefixed HTTP_ to mark them as client-originated. The proxy sets REMOTE_ADDR directly; a client trying to forge it would have to send a header literally named HTTP_REMOTE_ADDR, which the backend would parse as a client-set field, not a trusted one. The forgery is a different shape from a successful one. There is no header-name-collision attack in the FastCGI design, because it has domain separation built into the wire format.
FastCGI is a wire protocol, not a process model
The biggest practical objection people raise to FastCGI is that it sounds dated, and the reason it sounds dated is the historical association with the .fcgi per-request-spawning pattern that nobody runs anymore. Ayer's piece is careful to separate these. FastCGI today is just an alternate transport for HTTP requests over a TCP or Unix socket. In Go, switching is one import and one call:
l, _ := net.Listen("tcp", "127.0.0.1:8080")
fcgi.Serve(l, handler)
fcgi.Serve is a drop-in for http.Serve. The handler stays the same. http.ResponseWriter and http.Request keep their types. nginx, Apache, Caddy, and HAProxy all support FastCGI backends with one or two lines of configuration. The work to switch is roughly the same as adding TLS termination, with much less ongoing maintenance.
The piece also notes that Go's standard net/http/fcgi automatically populates the Request.RemoteAddr field from the trusted REMOTE_ADDR parameter, and sets the TLS field to a non-nil value when the proxy reports HTTPS. The middleware most Go services use to extract the real client IP from X-Forwarded-For is unnecessary. "It Just Works," Ayer writes, in one of the few rhetorical flourishes in an otherwise dry piece.
The honest downsides are real and Ayer lists them. FastCGI was never extended to support WebSockets. The tooling is thinner — curl doesn't speak it, even though it speaks FTP, Gopher, and SMTP. When Ayer benchmarked Go's FastCGI server behind various reverse proxies, some workloads had worse throughput than HTTP/1.1 or HTTP/2 — which he attributes to the FastCGI code-paths being less optimized rather than to anything inherent in the protocol. The cloud-era "just use HTTP, we'll handle it" mindset has not made room for FastCGI even though most of the infrastructure could.
Why HTTP won anyway
The historical question Ayer's post leaves implicit and the HN thread takes up directly is the more interesting one. If FastCGI is structurally better — better-framed, with native domain separation — why did HTTP win the reverse-proxy market?
The thread's most useful single comment frames it through the end-to-end principle. The argument goes: HTTP everywhere lets you compose intermediate gateways arbitrarily. You can run the same backend stack with a direct browser connection in development and behind a multi-tier proxy stack in production, without code changes. You can introduce a new authentication gateway, a DDoS filter, a TLS terminator, a region-routing layer — at any position in the request chain — without each layer needing to know what the others are doing. The end-to-end principle says: keep the network agnostic to what's being transmitted, push application logic to the endpoints. HTTP-as-everywhere-protocol is the literal embodiment of that principle for the web.
The piece's actual recommendation, the comment continues, is the principle of least privilege as the alternative. "Allowlist your communications to only what you expect, so that you aren't unwittingly contributing to a compromise elsewhere in the network." Don't trust headers you didn't ask for. Don't allow framing ambiguities you can't verify. Domain-separate the things that should be domain-separated.
These are both correct principles. They disagree about which threat is the bigger one. The end-to-end principle is what gives the web its compositional flexibility — the property that has, in fact, made the web outperform basically every closed-protocol alternative for thirty years. The principle of least privilege is what catches the class of bug Ayer's piece is about — the one where a Discord user's private attachments leak because two parsers disagree on a Content-Length edge case. Both classes of harm are real. The argument over which to prioritize is the argument over which class of harm hurts more, and the practical answer is it depends on what you're running.
A second thread in the discussion pushed back on the end-to-end framing entirely, arguing that connection caching and multiplexing — both of which the modern HTTP stack does compulsively — already violate the end-to-end principle in ways that explain most of the reverse-proxy exploits. The exploit class exists not because we picked HTTP, but because we picked HTTP and we cached connections and we multiplexed requests across cached connections, all the way through the stack, and pretended the resulting topology was still end-to-end. The desync attacks live exactly at the points where the pretence breaks down.
A third thread — citing Google's internal Stubby protocol, which wraps HTTP semantics in a different wire protocol for service-to-service traffic — observed that hyperscalers solved this years ago by not using HTTP between their internal services, while continuing to use HTTP at the edge. The compositional flexibility argument is real at the network's boundary. Inside the boundary, in the proxy-to-backend leg, the boundary doesn't exist anymore, and the principle is being applied where it doesn't belong.
That is, on reflection, the right way to read Ayer's piece. The argument is not "HTTP is bad." HTTP is fine for the browser-to-edge leg, where end-to-end composability is the whole game. The argument is that the proxy-to-backend leg is not an end-to-end leg, and using HTTP there imports a class of failure mode the leg should not have. FastCGI's wire format is what HTTP would look like if it were designed with the proxy-to-backend leg's actual constraints in mind: explicit framing, structural domain separation, no header-name-collision class. It is the protocol the leg should have been using all along.
What the production testimony says
The thread runs heavy on quiet production-engineering testimony. SSLMate's ten years on FastCGI is one data point. Others in the comments described running FastCGI for "all our web customers" for a decade. The most extensively-documented alternative in the thread was uWSGI, which one commenter said had been their reverse-proxy backbone "at several places for many years" with mostly-praise; another commenter described WAS (Web Application Socket), a separately-designed open-source protocol they built at CM4all roughly fifteen years ago, used in production at CM4all for hosting environments running PHP. Multiple commenters on different stacks converged on the same observation: when you run the same backend behind a non-HTTP reverse-proxy protocol for a long time, you stop having a class of bugs you used to have.
The counter-testimony was also concrete and worth taking seriously. One commenter described founding a Web 2.0 startup in the FastCGI/SCGI/HTTP era and choosing HTTP because "instead of needing to introduce another protocol into your stack, you can just use HTTP, which you already needed to handle at the gateway." The setup-cost saving is real. nginx came in "lots faster than most FastCGI/SCGI modules of the time, and more robust," which is also real, and the cost of switching to a less-optimized FastCGI path was eventually worse than the security cost of staying on HTTP. By the time the security cost compounded into a continuous run of desync vulnerabilities, the migration cost had compounded too.
Andrew Ayer himself replied in the thread, on a different sub-discussion about plain CGI, with the httpoxy footgun caveat — that CGI's use of environment variables to convey HTTP headers introduced a HTTP_PROXY-collision class that doesn't apply to FastCGI's parameter list. That distinction matters: the article is about FastCGI, not CGI, and the differences are structural, not just performance.
The interesting question
The interesting question is not whether your service should switch from HTTP-to-backend to FastCGI-to-backend tomorrow. For most services, the answer is the boring one: probably not, because the migration cost is real and the threat model can be managed by stripping headers carefully and keeping a current HTTP/2 stack between proxy and backend. The interesting question is why the industry's response to the request-smuggling class of bug — twenty years old since the foundational 2005 Watchfire paper — has been patch the wire protocol harder rather than use the wire protocol that doesn't have this class of bug.
The wire protocol you pick at the proxy-to-backend boundary is a security decision. We have been pretending it was a convenience decision for thirty years. The bill, in CVEs, has been correspondingly persistent. Ayer's anniversary post is a good moment to notice that the bill is still arriving, and that the alternative on the table is not new, not exotic, and not even particularly hard to deploy. Happy 30th birthday, FastCGI, he closes. Worth marking the year on the calendar where the question got asked again, even if the answer doesn't move the market for another decade.
Top comments (0)