97 comments

[ 4.0 ms ] story [ 167 ms ] thread
What's "serverless" if not basically CGI scripts?

(only slightly tongue-in-cheek)

(comment deleted)
Yeah, but CGI has fallen out of fashion and needed a re-branding!
And are we leaving performance on the table since jit based platforms never warm up?
Nothing, really. The application not having an in-process web server, à la CGI, is what 'serverless' refers to.

Although CGI is a defined protocol. Serverless is a more broad term, not limited to any particular protocol.

I run a number of sites on a single server behind a reverse proxy, and the fastest to load is a CGI service, cgit. I use Traefik as the reverse proxy nowadays, which doesn't support CGI directly, so I've bridged it with Go's net/http/cgo. Even with this extra hop, it the performance is still excellent.
web serving is "done"

you can serve most moderate-traffic services with any tech you like

its time for smart people to move on to other problems

This comment hurts, because it is true.
True enough, but that never stops people from reimplementing technologies badly, then working on speeding up their new solution...
This seems like a bit of a strange article to me. When we talk about being "slow", that is typically relative to a non-CGI technology, essentially something that keeps the code in memory ready to run. The reason CGI was "slow" was because 1) It had to start a new process for each invocation 2) When using tech that needed some form of warmup (I think PHP did??), you paid that for each invocation as well. In short, for some hobby or small setup it probably doesn't matter, and perhaps that is the authors point, but relative to an in-memory solution the situation probably hasn't changed much.
Also memory usage. An interpreter per active request would chew through RAM. I forget which Python project it was, but it introduced forking per request, and man that saved a lot of memory.
Both points could be addressed by using a file system cache (or RAM disk) and a compiled language. A CGI application could, for example, be written in Go (https://pkg.go.dev/net/http/cgi).
CGI as in a command line program lying on the disk, executed on every request and the output sent to browser is dog slow.

Convert this one to fcgi, and it will get as fast as any other solution. In fcgi mode it runs as a service listening to a socket and reads and sends response over it while never terminating.

Even php running on fcgi mode is far faster than the old styled mod_php.

If you have any reasonable OS and enough ram, the sectors your program actually uses will be in disk cache. Sure, there's maybe some syscalls and vm faults to get it mapped into the process, but it's not dog slow.

If you don't have enough ram for it to be in the disk cache, chances are your fcgi daemon is going to get swapped out too.

Now if your program insists on taking forever to start up, that's something that a long running daemon can help.

I haven't benchmarked, but I really don't see why php in fcgi mode would be much faster than mod_php. I've run both modes and they're both very fast. But then someone uses one of the popular frameworks and hello world takes 20-40ms, so it barely matters how you run php.

Even with warm disk caches, interpreted languages tend to have pretty slow startup times, because they spend a lot of CPU time doing the same stuff over and over again. On my machine, the fastest of a dozen runs of just the interpreter is 66ms for Node.js (`time node -e ''`), and 14ms for CPython (`time python -c pass`). And my measurements a few years ago had overhead of about 1ms per module/file in Node.js, so if your source is spread over 100 files (which is honestly unrealistically low if you use any popular libraries and don’t bundle anything), your typical startup floor is now 200ms of CPU time, apart from disk time, on a good day. And some of those modules probably incur other costs, too.

I have no figures on PHP, though if its execution model hasn’t changed from a decade ago most of this probably won’t affect it much because it effectively didn’t even persist modules. But I don’t know.

As always, don't use languages that suck.

On my system in hosting (FreeBSD 13.2-RELEASE, 2x Xeon L5640), time perl -e '' with perl5-5.34.3_3 reports 3ms. On the same system time php -r '' takes 16 ms with php82-8.2.11 and a handful of libraries installed. By my own criteria, php as a cgi sucks; but I did say if your program takes too long to startup, run it as a daemon, which I think includes linking it in like mod_php or mod_perl.

On the topic of mod_php vs php_fcgi; with apache mod_php, an empty php file takes about 1.2 ms to serve, as measured by curl from when curl sends the GET request to when the content ends. I don't have a php fcgi server to try, but my recollection was something similar for minimum php output from lighttpd with php_fcgi on newer hardware.

If you run an actual binary, the OS will not only cache the code pages, it will share them across processes. In the perl case, the actual interpreter is loaded, cached, and shared across the CGI invocations, while the actual script code will be cached from the disk, but each invocation will (likely) get their own copy of the code (i.e. they will duplicate it from cache when they read it in).

Finally, of course, the runtime needs to start up again, compile/interpret the code, and, finally, execute it.

A binary will have the fastest startup. I'm sure there may be things that could be done. I don't know if perl mmap'd the .pl file as a readonly buffer, if each invocation would share that file (probably), and thus prevent that initial duplication. Don't know if any of the interpreters do this. Odds are this gain is lost in the whole startup of the interpreter anyways.

Also, using a binary, by a similar mechanic, lowers overall memory consumption. Have 100 connections all running the binary, they all share the code pages, and thus only "pay" for their individual data use. Share 100 connections with an interpreter, and they each pay their own cost for the entire program file plus any runtime data. Again, not so much a problem today with our ample memory resources, and mmap the source may remedy this, but this actually was an issue back in the day.

It may not be about any language implementation inferiority, but rather a question of what they’ve optimised for.

Node.js is firmly designed for daemon operation, where having a high cost of instantiating a module, something that you only do at startup, is fine, if it means you later get better throughput because you’ve done JIT compilation or whatever. (It may also just be that Node.js is unnecessarily slow at these things because no one’s cared enough. But module loading being slow is reasonably well-known in the ecosystem at large so I doubt it’s particularly that.)

PHP might have lower engine startup costs because of its history of CGI usage, and lower code loading costs because of its start-from-scratch-for-each-request model—but both are probably at a later runtime cost.

Perl might have lower engine startup costs because it’s tended to be in a similar basket to PHP, plus more general scripting use, where you just want to get through things quickly: faster for one-offs, slower for repeated things. And my understanding of Perl is that it’s somewhat closer to just being an interpreter (which is excellent for latency but terrible for throughput) than are the other languages we’re discussing.

somewhat off-topic, but I want to share hyperfine[1], which is nearly purpose-built for measuring this kind of performance:

    $ hyperfine --shell=none "node -e ''" "python -c pass" "perl -e ''" "php -r ''" "bash -c ''" "echo"
    Benchmark 1: node -e ''
      Time (mean ± σ):      87.3 ms ±   8.0 ms    [User: 69.8 ms, System: 18.4 ms]
      Range (min … max):    77.9 ms … 111.1 ms    31 runs
     
    Benchmark 2: python -c pass
      Time (mean ± σ):      24.4 ms ±   5.6 ms    [User: 17.5 ms, System: 6.7 ms]
      Range (min … max):    15.8 ms …  31.3 ms    97 runs
     
    Benchmark 3: perl -e ''
      Time (mean ± σ):       2.8 ms ±   0.6 ms    [User: 0.9 ms, System: 1.8 ms]
      Range (min … max):     1.7 ms …   3.9 ms    863 runs
     
    Benchmark 4: php -r ''
      Time (mean ± σ):      19.3 ms ±   4.2 ms    [User: 8.9 ms, System: 10.0 ms]
      Range (min … max):    12.1 ms …  24.8 ms    241 runs
     
    Benchmark 5: bash -c ''
      Time (mean ± σ):       4.9 ms ±   0.8 ms    [User: 2.8 ms, System: 1.8 ms]
      Range (min … max):     2.6 ms …   6.4 ms    552 runs
     
      Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.
     
    Benchmark 6: echo
      Time (mean ± σ):       1.2 ms ±   0.3 ms    [User: 0.7 ms, System: 0.3 ms]
      Range (min … max):     0.8 ms …   2.1 ms    2237 runs
     
    Summary
      echo ran
        2.41 ± 0.89 times faster than perl -e ''
        4.17 ± 1.39 times faster than bash -c ''
       16.37 ± 5.91 times faster than php -r ''
       20.74 ± 7.64 times faster than python -c pass
       74.21 ± 22.49 times faster than node -e ''
(tested with node 21.5.0, python 3.11.6, perl 5.38.1, php 8.2.14, and bash 5.2.21 on Ryzen 2700X)

[1]: https://github.com/sharkdp/hyperfine

You don't have to use an interpreted language with CGI.
Interpreted languages often starts slow, yes. That is an issue with interpreted languages, not with CGI as a concept.

If you trade low/no build time for high startup latency, by all means stay clear of CGI at all cost.

I've built fast CGI based systems, but not recently, because I prefer the convenience of an interpreted language, so I am not arguing you should prefer CGI.

Just that comparing a design pattern that is a consequence of not using CGI is a poor measure of CGI. If you were to seriously want to use CGIs, you wouldn't use runtimes that slow to start, or spread everything over hundreds of files, at least without a build step.

You still have to build the process image. I suspect for anything simple enough to run as a CGI app, the instruction count to build the process image is as long as the instruction count of the CGI app itself.
It really depends on what you're doing. Resizing images is a pretty common CGI application, and that's going to spend a lot more time on compression than it does on process setup (one hopes). Process separation is a good idea for image processing, as codec processing has high risk.
It is stored in cache so the system hardly ever goes to the disk. When you run some command on Linux like "grep", most of the time it is running out of cache.

A real weakness cgi has is that if your application involves a lot of in-memory configuration that has a terrible impact on startup time even if it has an excellent effect on execution time.

> It is stored in cache so the system hardly ever goes to the cache.

Typo?

Thanks for the catch. I fixed it.
https://fastcgi-archives.github.io/

fastcgi c/c++ server development was essentially stopped many years ago, would love to see a c version that I can use for embedded boards, though cgi does the job 90% of the time just fine.

FastCGI was feature complete decades ago, there's little development needed for shiny new features.
Apache’s mod_fastcgi’s last commit was 2 weeks ago:

https://svn.apache.org/viewvc/httpd/httpd/trunk/

It’s a fork of what you linked (and was more popular afaik back when fastcgi was state of the art, and apache was the undisputed champion of web servers).

These days, nginx has more market share than apache, and its fastcgi module is one of the more recently updated ones in its source tree (5 months vs multiple years):

https://github.com/nginx/nginx/tree/master/src/http/modules

If I was going to build an embedded web server, I’d start with nostd rust, probably with though axum + tokio, since thats already memory safe-ish.

If I needed fastcgi for some reason (dynamically loadable endpoints, or os-level isolation), there are at least four implementations of fastcgi for it. No idea if any are decent though.

Seems pretty well established, and started a long time ago, could it be feature complete and not need as many commits?
> In fcgi mode it runs as a service listening to a socket and reads and sends response over it while never terminating.

What's the point of using this over serving HTTP directly? The whole point of CGI was providing an easy to implement interface to proper HTTP servers, but FastCGI is such as step up in complexity compared to CGI that you're going to need a library anyway.

Maybe FastCGI made sense back when companies tried to convince us we needed their heavy-duty software to handle any HTTP traffic at all, but now HTTP libraries and reverse proxies are dime a dozen.

HTTP daemons remain pretty nice, and if you use them rather than serving HTTP from your language (... and how many languages are you using? Each HTTP-serving lib is another liability) you get a lot of cool stuff "for free" ("just" config against a battle-hardened system, not code)
The two aren't mutually exclusive.

* I agree that you often will want a server such as Caddy/nginx/Apache httpd to be the Internet-facing http server. They give you a lot of nice things like DoS resistance, logging, robust TLS implementations, and dispatch to all the various applications you might be running.

* But you need some protocol between that proxy and your application. Like debugnik, I'd prefer to just use HTTP as that protocol rather than something else like FastCGI. I already have to be familiar with HTTP/1.1 in the sense that I both actually know the wire format by heart and know of a huge variety of tools that can directly speak it. So I'd rather just have my application speak HTTP over a Unix or IP socket that my frontend webserver proxies to. "Each HTTP-serving lib is another liability" is at least as true for "each FastCGI-serving lib" given that the FastCGI libs will be obscure (less well-tested and maintained) and (due to poorer familiarity/tooling) harder to debug.

> So I'd rather just have my application speak HTTP over a Unix or IP socket that my frontend webserver proxies to. "Each HTTP-serving lib is another liability" is at least as true for "each FastCGI-serving lib" given that the FastCGI libs will be obscure (less well-tested and maintained) and (due to poorer familiarity/tooling) harder to debug.

True-ish, but complicated a bit because (as far as I can tell) the FastCGI spec is tiny compared to HTTP... even just HTTP 1.1, let alone 2 and (yikes) 3.

> True-ish, but complicated a bit because (as far as I can tell) the FastCGI spec is tiny compared to HTTP... even just HTTP 1.1, let alone 2 and (yikes) 3.

That's true-ish also. For this back leg, it's fine to only support HTTP/1.1; the frontend server can handle translation from HTTP/{2,3} as necessary. And the HTTP/1.1 spec defines a lot of stuff like the syntax/semantics of the headers and semantics of different request methods that the FastCGI spec doesn't address. It's still probably true that the HTTP/1.1 wire format a bit more complex than FastCGI, but it's much closer than it might first appear. On balance I'd still take HTTP any day.

My reply would be pretty much what scottlamb said already. Of course HTTP daemons are nice, but these days they can just reverse proxy an actual HTTP service on a given route instead of using some knock-off protocol like FastCGI; you need a protocol and library either way, so FastCGI isn't an advantage there.
My experience is that they almost always end up being "just" a dumb pipe, then. Routing (beyond proxying requests to backend servers), request logging, bi-directional interaction with the backend code that can enable cool stuff like conditional direct-serving of files, error handling, all kinds of great stuff available with server modules—that all ends up unused, because the HTTP daemon and the "application" end up different places, and making them work tightly together to let each do what it's best at becomes impractical.

[EDIT] And, as noted in my response to the other reply here, the size of the FastCGI spec, and the size of implementations, are far smaller than HTTP, which is nice.

A few years ago I developed a websockets-based RPC protocol for connecting Go application workers with a higher-performance C++ networking supervisor, to workaround scalability issues in grpc-go. When I got done with it I realized I had reinvented fcgi. It's really a quite OK protocol that is so simple it can't become obsolete.
How slow is “dog slow”? And for some purposes “dog slow” is acceptable.

I have an internal app that runs as a CGI but is written in a compiled language. It averages 100ms response times, mostly due to database queries.

I wouldn’t write a major app using cgi but for small sites and internal services it’s fine

No, it's not - at least not inherently -, because unless you're extremely memory constrained your program will be cached in memory.

If it's then also prepared properly, e.g statically linked position independent code, you won't even have relocation overhead and the only thing you "pay" is the process creation overhead, which hasn't been a major issue on at least Linux for a couple of decades at least.

Whether the difference will matter really depends on your specific runtime. With an interpreter, startup can easily bite you hard, sure.

PHP is a bad example for that reason - the moment you need a bunch of extra syscalls on startup, the context switch latency alone will kill performance vs. what you can achieve.

> Yes, your CGI may slow down if you're getting a hundred hits a second. How likely is that to happen, and if it does, how critical is the slowdown?

It's slightly ironic to be reading this phrase via the front page of HN.

It's a good point, though, for the many developers who are writing software for a limited audience, such as internal tools. Can't be reached by HN, can't be hugged to death.
Speed of iteration is most important for most development in the beginning.
The site is running as a cgi script, according to the article, and hitting the HN front page doesn't seem to have slowed it down in any way that I've noticed. So I think the point is well made.
The page was served to me with `Last-Modified: Thu, 28 Dec 2023 04:02:49 GMT`, which to me implies either aggressive caching, or that the publishing system acts like a static site generator.
(comment deleted)
The publishing system takes some pains to provide an accurate last-modified value for HTTP caching purposes (and an ETag too); internally it tracks the most recent modification time of all components that go into the page as it's (dynamically) assembled from various pieces.

(I am the author of the linked-to article and also the author of the software it's running. Said software also has (on-disk) caching, but that's not why the last-modified is back in December of last year.)

Actually, an article of mine was on the front page of HN the other day, and it only got ~10,000 hits over a day. At its peak it was less than 1 per second. I've been #1 on HN before, and you still only get about 30k hits total with a peak of several per second. So even if your responses take 200ms and you can only handle one at a time, you'll be fine. The HN/Slashdot effect is not hard to handle!
I was also on the front page earlier in the week, and I agree with your numbers. Let's call it 1/s peak.

If a page is using CGI, it's often (but not always) because it's interactive in some way. If users are interacting and generate 10 requests total, on average, you're down to only 100ms per request (which is about how long it takes my system to spin up a new python3 process and `import numpy`).

> A real Python CGI would take longer to start because it has more things to import, but even then it's not necessarily terribly slow.

That's where the author is wrong. Complex Python, Ruby or Perl scripts can easily take several hundred milliseconds just to import required modules.

“not necessarily terribly” is a little weasel-wordy, but it does, I think, give them enough space to consider hundreds of milliseconds as not a big deal.

Edit: see the responses for why “not necessarily terrible” is kind of annoying, now I have a comment that says there exists a use case where they care about it, and there exists one where they don’t.

It might not be consistent to compare the entire request lifecycle instead of the original comment in only firing up.
Hundreds of milliseconds can be "not a big deal" depending on what your program is, how often it's run, etc.

The last time I used CGI was for the "sign up to my newsletter" on my website; the endpoint for that would lock a file, append the email from the form, and close it. Even if that takes a second (which it didn't, more like 50ms) then ... that would actually be fine for this particular use case of a small newsletter.

Of course for many other things it would be horrible.

I think "not necessarily terrible" is a reasonable way to phrase things.

Depends on the load levels or payment schedule. If we are talking about AWS Lambdas, hundreds of milliseconds are costing real money, which is why I write my Lambdas in Go instead of Python. That way we keep their exec time under 100ms which became unachievable with Python for the kind of code we're running. I love Python, but I don't love it enough to pay double or quadruple of what I pay for running Go code.
You’re right about the time is money dynamic but Lambda won’t hit this particular case because your imports will happen before the Lambda function starts receiving requests. I use that for things like retrieving SSM parameters and other setup work involving network requests.
That’s the “not necessarily” part - Python takes under 20ms to start and import a few modules. If your needs are modest, there’s a solid argument for simplicity there. If not, it is not a huge lift to move from that to a persistent server model where you could do big imports once, either using the stdlib HTTP server or switching to Fast CGI.
(comment deleted)
I would say that CGI might be cheaper in terms of CPU usage on cloud, as on idle it does not use CPU more than the system. I wonder what is the "idle" cost of java running spring for example. All I know is that if I run my application I am developing right now, the laptop I am using is trying to fly away (It's already warming up the engines), but it has also a postgres in another docker
An idle persistent process doesn't use any CPU either (unless it has set up some background work) so there really shouldn't be much difference.

I think at the extremes process-per-request and persistent processes look very similar.

A fresh process will read data from the disk cache, but it may need to parse or transform that data before it is usable. A persistent process will have (some) pre-parsed data in memory. If you have swap enabled it will also be swapped out to disk if the process is idle. This may be faster if parsing that data into an in-memory structure is slow or slower if this results in more IO than the source data (which may be stored more efficently).

The reason why CGI is slow is because it is missing many things that would typically be cached in persistent processes. Loading and starting the executable or interpreter can be slow (especially if you have lots of imports) but that probably isn't the biggest issue. Bigger issues are backend connections such as databases or external APIs. If those are on localhost than it is entirely possible that CGI will perform well enough for your application. But even if they are a handful of milliseconds away it can add up real quickly.

If you don't have these issues than by all means use CGI. There is nothing wrong with it. But at some point having an in-memory cache is just going to be faster, even if your executable and various other dependence are cached by the OS page cache. Your in-memory "cache" will have all of the common data it needs pre-parsed and ready to use and may have backend connections already warmed up.

Isn't web-functions/lambda going to have similar issues? They'll perform well enough, if they aren't terribly complex, but adding to many dependencies start up times could be an issue.
Some web function runtimes like AWS Lambda let you put dependency initialization outside the function itself. Depending on what the runtime does, you may get to keep these cached values for future invocations.
Sort of, but lambda-like runtimes typically act like something in between process-per-request and a long-lived persistent process.

AWS lambda at least will reuse the same execution environment for multiple requests. So simple things like caching in a hashmap or using a database connection pool will work and avoid these startup costs.

You still get CGI-like behaviour on cold starts but once warm they perform much more like a persistent process. This ends up being a pretty good trade off for many use cases because you use little resources when idle and get pretty good latency and resource usage when busy.

From my experience with a slow start AWS lambda, the cold start times really bite you every time you have a spike in concurrent requests, even a small one. We had a pretty pathological case (a Python lambda with rather a lot of code which also required a network adapter to be deployed inside a VPC), but the point still stands even for better behaved lambdas: if you have a steady 1000 concurrent connections say, you'll be OK for days. But if then 100 more concurrent connections need spinning up, you'll have 100 users taking the slow path, which may be their first experience of your service.
Yes, it is a tradeoff. I don't know about AWS Lambda specifically but this sort of behaviour can be mitigated with pre-warming. So you always maintain N% extra capacity. Then you allow the instance to start up before actually sending any requests there. Of course if you scale up >N% faster than your instances can start up you will still hit a cold start, but if your app starts relatively quickly and you maintain a decent buffer that should be fairly rare.
I wonder what percentage of users would place the blame on your service? I know my entire life I've had unreliable Internet connections and so my instinct is to first shake my fist at my ISP.
Yes, and Lambas have become far more complex than CGI.
I think the problems you highlight are mostly orthogonal to CGI. You absolutely can use an in-memory cache with CGI. You could even use shared memory if you were so inclined.

Startup cost is potentially an issue, but the trick there is to make sure the CGI bits are tiny and can delegate heavy duty work to longer running processes. You don't have to execute the entire workflow within the CGI script.

Are there any advantages to CGI talking to another process (IPC I guess?), or is it just serving traffic with a long-lived process, but with extra steps?
Basically a long-lived process with extra steps. But if all it does is hold a few file descriptors open it can minimize your long-lived state (which can reduce bugs) and in-use memory between requests.
Aside from simplicity, there aren't any major advantages AFAIK. Even in 2024, it only takes about 5 minutes to set up a system with Lighttpd and CGI. When all you have to do is expose an existing binary over the web, then HTTP<->CGI<->Binary is pretty convenient. Just write a tiny CGI wrapper and you can call it a day.

Edit: missed a bit of context, so here's a more relevant answer:

Long lived process with extra steps, yes, but the key part is that the CGI binary (and the webserver) are sacrificial. They handle input validation and all that nasty stuff, while the real application is safely tucked away. If the CGI binary dies, or the web server crashes, things become unreachable but otherwise operational. It's occasionally a useful characteristic.

Depends how much you push over the process boundary. The main benefit of a CGI model is guaranteed cleanup of resources, so the more you keep in the CGI side of that boundary, the less potential you have for resource leaks, or data leaking between requests. If you just push your entire app into a long lived process you gain nothing. If you e.g. "only" use it to cache resources that are slow to obtain access to (e.g. connections to slow starting resources, data that is slow and expensive to parse), then it could be a win.

But it's been years since the last time I felt that win was sufficiently worth it to design a system that way. In theory, I still love the idea of a guaranteed clean slate, but I'm not willing to give up the slow starting language runtimes I prefer today (e.g. Ruby) for the sake of it.

CGI weren't all that slow even 25 years ago if you minded exactly the things you mention.

That is: Compiled, statically linked code, that also took advantage of the fact the process would die (which meant e.g. not freeing memory, or doing any other unnecessary cleanup), and get external APIs out of band where possible unless their latency were low.

I built a webmail service like that back then, and we measured vs. FastCGI and other options and it performed well.

Wouldn't necessarily do it again, because it was a product of competing against slow interpreted languages on a whole server park slower than my laptop, where you could recover from the process creation overhead just by being faster.

Things like the static linking gave us a double digit percentage reduction in latency over the "naive" CGI approach, and cut the advantage of persistent processes right down. Being able to drop almost all memory frees helped a lot too (we'd only free particularly large objects, otherwise just let the OS free everything at once when the process died)

Overall we were mostly IO bound despite using CGI even with the significantly worse process creation overhead of Linux back then (it was never "bad" compared to e.g. Windows as far as I remember, but there were significant efforts to improve it)

It seems kind of obvious that CGI can’t be ‘slow’ in modern terms.

We were serving hundreds of hits per second on 200MHz web servers in the late 90s. Everything is _so much faster_ now that surely the performance must be acceptable nowadays?

Really! Reading this thread makes me feel like I've been sent back in time. Just wait till we get NSAPI/ISAPI released!
I always stuck to plain ole CGI in my Python/Windows systems. They were relatively fast enough and, well, the "faster" stuff just wasn't well-documented for the Python/Windows ecosystem.
A shitload of stuff from the Bad Old Days is "not particularly slow" now that everything's in cloud-whatevers and k8s clusters.

Anyone remember when you offloaded something to the server to make it faster? Now it's like "well this is blazing fast on my machine, but we'll see how it does when it's deployed to prod..."

I always joke that cloud is Tanenbaum's revenge.

All that monolithic kernel performance wasted in endless layers of virtualization.

Every time I use some cloud-thingy that's just fucking scheduling, running, and managing multiple processes, and it's way harder and more error-prone and less reliable and burns a multiple more person-hours to set up and manage than just, I don't know, doing that on an operating system, which is a thing specifically designed for that kind of work, plus performs like dogshit, I die a little inside.

[EDIT] Incidentally, I don't hate all this stuff—I like docker-as-a-cross-distro-package-and-process-manager better than most of the rest Linux has to offer on that front, I find k8s kinda-nifty, but wish it 1) didn't always seem to involve so much goddamn yaml, which is so shockingly horrible that I can't believe it's still in so many things, and 2) were better-integrated with the underlying operating system(s) rather than reinventing so very many wheels and being another layer, rather than part of the system. Like I can vaguely imagine, through a hazy cloud of dream, some kind of hypothetical k8s-alike integrated into FreeBSD that I'd absolutely love.

I think it is one of those things where even if it is "fast enough" in most cases, why not just use one of the readily available faster options anyway? It's not like setting up FastCGI is onerous. Apache/Nginx front-ends to a FastCGI process are as straightforward as you could ever want and the vast majority of languages you might use in this configuration have decades old battle-tested libraries and tooling.

In fact, I drastically prefer having no front-end requirement. Even if node is "slow" compared to some C FastCGI benchmark, I like the operational simplicity of deploying code onto some stripped down Linux with minimal explicit dependencies. Go offering a single binary is even more attractive. I was always frustrated running Apache/Nginx, then maybe some process manager like PPM, fighting with the configuration across a bunch of different formats.

While I admit that modern container based orchestration can be overly-complex, it doesn't leave me nostalgic for the days of CGI.

My home server is all CGIs in Lua, Python and Ruby. Some endpoints have less than one hit per month. I don't care much about how many milliseconds I can shave from the startup time as well as from the run time. It will be fast enough whatever I do.

For my customers, no CGIs. They do run on Python and Ruby though.

Could you speed up CGI by, say, having an array of processes ready and waiting to receive data?

Like an array of Haskell CGI processes or something...

Not as CGI is currently designed. Many components of the request, like the HTTP headers, request method, and path, get passed to the CGI script as environment variables.

You could redesign CGI to change this, of course. But at that point, you've lost compatibility with existing CGI scripts, and you might as well use FastCGI and/or HTTP over a socket instead.

With some hackery you might be able to do it even with plain CGI. I'm thinking doing something like user-land exec[1] and pausing the execution of child just before `main` (or whatever entrypoint). You still need to do one fork per request, but still you could improve average case latency by having children pre-forked.

No, it absolutely would not be worthwhile to go through such hoops, but fun thought experiment.

[1] https://news.ycombinator.com/item?id=37025843

This is the Fast CGI model, using pre-forking in something like Apache. Apache creates a new process for each incoming connection, but pre-forks them (so they sit idle without a connection), and when those processes fire up, Fast CGI starts up the "cgi" program and just waits for the data. After the request, the process and cgi program remain and linger, ready for the next request.
With FastCGI, CGI can be performant on humble hardware. I run smokeping via FastCGI on a Raspberry Pi Zero W (a $10 SBC). Response times are very good.

CGI's biggest weakness is the IO & CPU needed to "boot" the interpreter. If you can keep the interpreter resident, you can reach acceptable performance.

I argued in favor of CGI scripts over a decade ago as more web work was being switched to PHP or mod_perl.

Even on "slow" hardware, they could often run in about a second. Which was still fast enough for a small business with a form that gets submitted a few times per day.

For a lot of a small sites, needing to serve more than one request per second would be a great problem to have.

Right, so maybe you can help me understand. Literally the only thing I've seen anywhere, both in TFA and in all the responses (including yours) is "CGI isn't really that slow."

So that's a reason not to not use it; but are there any reasons to use it?

Even in TFA, he demonstrates using a simple Golang CGI binary; but writing a simple Golang server that you pass through as a reverse proxy is just as simple.

CGI is a good fit if you don't maintain the server it runs on. You've got a basic "user" account and can just upload a CGI script. For example, you've got a static website and need a simple contact form handler.

Apache had an option to run the scripts as your user, rather than default of "nobody". That allows segmenting the processes of different users on the same server.

No memory leaks (unless you use shared memory). No weird connection pooling and caching issues or delays in picking up a config change. Every request is a fresh start, enforced by the OS. Buggy code (like image processing can only crash the current request).
If you avoid writing the simple server you can assert your familiarity with serverless on your resume.