Fast enough
Contents
Efficient resource utilization was at the forefront of computing from the very beginning. Over time we traded some of that efficiency for developer experience, and preserved the rest by harnessing better hardware and new paradigms: multithreading and multiprocessing, stream processing , asynchronous programming… Languages are how we reason about problems, encode algorithms, and organize programs — the vector runs from high-level languages through structured programming , OOP , functional and logic programming . Note what that vector does not include: how the runtime is structured, compiled or interpreted — either way we write some text and hand it to the system. Each step looks logical in hindsight. Yet some things don’t look logical at all.

Hypothetical client-server application
Let’s imagine the classic client-server application: a server and a number of clients, both running custom code specific to our application. Say it is modeled on web applications: we fully control the server, but we don’t control the clients — users come with whatever they have: phones, tablets, desktops, and so on. Ultimately our code runs on a CPU either directly (compiled) or indirectly (interpreted or JITted).
Client side
For the clients, our code is frequently a one-off, even if we force them to install our application. Either way, the user brings their device and pays to run the client’s part of our application: the amortized price of their hardware, plus bandwidth and power. On the server, the code runs constantly, and we pay for it. I’ll assume we want to optimize our expenses as much as possible: pay less, serve more.
One important wrinkle: users have a zoo of devices with a multitude of CPU types. We can check a device’s capabilities dynamically — memory size, display properties, network speed, and so on — but what should we do about the CPU? The code has to run on it. So we have a few choices:
- Ship source code (JavaScript
) or some standardized bytecode
(Java
), then:
- Interpret what we got.
- Use a JIT compiler plus an interpreter.
- Compile locally, then run.
- Pre-compile on the server:
- The client gets correctly compiled code for its CPU, negotiated by some protocol.
- Each CPU/OS combination is compiled once, then cached and served on demand.
- Problem: can we compile for all possible clients?
As we can see, there are good arguments for interpreted languages on the client. They also let us support new clients on different OSes and CPUs without compiling for each one.
On the other hand, there are arguments for pre-compilation. If we have millions of clients and our client code is reused constantly — say, as an application for a mobile platform — it makes sense to pre-compile once. With millions of clients spread across a hundred-odd CPU/OS combinations, compiling once per combination instead of once per client saves four orders of magnitude. Another consideration: a client device may be capable enough to run our app but not to compile it, let alone optimize it properly.
Server side
We have a relatively small number of servers (compared with clients), and usually we get to choose them. The host type is typically fixed, even in a clustered environment where the provider may migrate our virtual machines around. Randomly heterogeneous environments are theoretically possible but extremely rare — I haven’t met one in production yet.
This means we can compile our server code with the best optimizing compiler available, with all the bells and whistles: static analysis , vectorization (using SIMD ), automatic parallelization , interprocedural optimization , and so on.
None of this is to knock JITs, profile-guided optimization (PGO), or other runtime techniques — but most deep optimizations cost significant resources, and therefore time, so we usually cannot afford noticeable pauses to optimize on the fly.
In my experience — and I have run a few comparisons — compiled languages were always faster than interpreted ones on realistic algorithms and workloads.
Yet compiled languages have a drawback: they must be compiled first, and with super-optimizing compilers that takes significant time — more or less ruling out compiling on demand — whereas interpreted languages can start almost immediately.
Back to client-server split
So if I were optimizing for efficiency, performance, and cost per transaction, I would go with interpreted languages on the client (realistically JS, HTML, CSS) and a compiled language on the server (C++ ? Rust ?).
Yet only half of that happened. The client went as predicted — JS, HTML, and CSS own it to this day. The server is where reality inverted.
Java
Interpreted languages ran on servers long before Java — Perl comes to mind immediately, and later PHP . But — a few famous Perl deployments like Slashdot and IMDb notwithstanding — they were seen mostly as tools for low-volume services: the kind of thing a programmer would set up in their spare time to host a blog.
Then in 1995 came Java. Its pre-1995 history is interesting, but it became Java in 1995 — the year the internet became big. Existing browsers were clearly insufficient for interactive content, and Java seized the moment with Java applets , small programs running inside the browser and showcased by Sun’s own HotJava . (The unrelated JavaScript arrived in December 1995, named after Java purely for marketing reasons.)
Java applets never became popular. Several things sank them — security holes, the browser-plugin model — but the one that matters here is startup. An applet dropped Java into exactly the transient-client slot, and as a pure interpreter Java fits that slot: no client-side compile step, ready to run. What it could not do was start fast enough. A JVM has to load, verify, and link thousands of classes before it reaches the first line of the program — work the specification mandates for safety and portability, redone on every launch. For a page that has to appear now, that cost is fatal.
On the server the process starts once and runs for weeks, so that same startup barely registers — and there Java flourished, helped by factors that had nothing to do with raw speed: the simplicity of the language and its execution model, the ease of porting the JVM to new platforms, a major marketing push, and good timing alongside other innovations.
Java can also be precompiled to native code ahead of time — for example with GraalVM Native Image , or, increasingly, through OpenJDK’s own Project Leyden — aimed as much at that startup cost as at raw speed. Tellingly, they reach it not by changing the specification but by caching the load-and-link work ahead of time, or by leaving the dynamic model behind altogether: the per-launch work is identical every time, so the lever is to precompute it. On the server, where startup is a one-time event, the default still remains JIT compilation at runtime.
The startup story did not end there. A constantly running server can shrug startup off — what matters over weeks is the long run: no memory leaks, no resource leaks, not how fast the process comes up. But serverless platforms recreated the transient slot on the server side: a function may be spun up for a single request, and suddenly cold start is the metric again — which is exactly what pushes Native Image and snapshot-restore schemes like AWS Lambda SnapStart into production. The transient slot turned out to be a situation, not a place: wherever code runs briefly and on demand, the applet’s problem returns — and precomputation returns as the answer.
Installable apps
A mobile app sits between the web page and the server. Like a web client it runs on hardware we don’t own, across a zoo of devices; unlike a web client it is installed, and that install step is a slot where compilation can happen. So the platform gets to decide where on the spectrum its apps land — and the two big ones chose opposite corners.
Android ships bytecode and compiles on the device. Its Java application layer walked the whole spectrum over a decade: Dalvik interpreted bytecode (2008); Froyo added a JIT (2010); ART introduced ahead-of-time compilation at install time (KitKat, 2013), replacing Dalvik in Lollipop (2014); and when compiling everything at install proved too expensive — big apps took minutes to install, and every OS update recompiled every app — Nougat (2016) settled on a hybrid of interpretation, JIT, and profile-guided AOT. A phone’s cycles cost the user directly — in battery and heat — so the pressure runs toward precompilation; yet the endpoint is not pure AOT but a hybrid that compiles mainly what gets used, the same middle ground reached from the other side. (The lineage is fittingly Java-soaked: Andy Rubin , who co-founded Android, had earlier built Danger’s OS for the Sidekick — one of the very few operating systems written in Java down to its device drivers.)
iOS took the opposite corner. Apps are compiled ahead of time, by the developer, straight to native ARM machine code — no bytecode to interpret, and no JIT: the system will not let a third-party app generate executable code at runtime. (An app may still embed an interpreter — plenty ship JavaScript or Lua — but the platform’s own path is native from the start, with nothing to warm up.) The store does the per-device work instead, slicing each download to a single architecture and its resources. For a few years it went further: with Bitcode , developers uploaded an LLVM intermediate form and Apple recompiled and re-optimized each app server-side, per device family, the optimization amortized across the whole install base — the precompile-on-the-server case made real. (Apple deprecated Bitcode in 2022; the slicing remains.)
Same slot, opposite answers: Android pushes the compiler onto the device and lets it adapt to how the app is actually used; iOS keeps it ahead of time and central, and bars the runtime kind outright. What they share is the install step — the one thing the transient web client never gets, and the reason neither is stuck interpreting everything in production.
WebAssembly
The standardized-bytecode option from our client-side list got a second act: WebAssembly is the Java bytecode of our times. Its UI story is still missing — in the browser it reaches the page only through JavaScript — but for pure computation it already delivers what the applet promised: one portable binary, compiled ahead of time, running in the same transient slot — Figma’s rendering engine and Photoshop on the web are built on it. The applet it succeeds promised computation and UI, and died; Wasm survives in the same slot by promising less. And it is not staying on the client — WASI points the same bytecode at servers and edge runtimes.
SQL
SQL is the same story in a different costume. It is a declarative language: a query says what result it wants, and the engine’s query planner works out how to produce it — in effect interpreting each query at runtime. That flexibility is wonderful for a data analyst running ad-hoc queries, but most programs don’t need it. They issue the same handful of statements over and over, hard-coded or built by an ORM into essentially static shapes.
So databases do what our client and server did: they precompile the hot path. A prepared statement reuses a plan instead of building one on every call, and a stored procedure keeps a compiled execution plan ready to run. Interpret for flexibility, precompile for speed — once again the answer is both.
I watched this play out early in my career. We built our server-side apps with hand-written SQL — no ORM — and once an app was finished, verified, and certified, the older, bearded database administrators would come in, rip out our hard-coded queries, and replace them with stored procedures for production. And they were exactly right, moving the hot path from interpreted statements to precompiled ones. The tooling has since turned that into a feature: Django
, for one, lets a team prototype against the ORM and later call a stored procedure through the cursor’s callproc() — flexible layer first, compiled query when it matters.
I/O bound?
The reasoning above assumes the server is CPU bound. It usually isn’t. Most web applications are I/O bound — the server spends the bulk of its time waiting on the network, the disk, the database, or another service, not computing. When computation is a sliver of the wall-clock time, making it twice as fast barely moves the total. A compiled language can win the CPU benchmark outright and still be invisible behind the latency that actually dominates.
Getting that concurrency right took the industry decades. The earliest network servers simply forked a copy of the process for every connection — clean, but forking is expensive. Lightweight processes on Linux and threads on Windows (and, later, both everywhere) made it cheaper; pre-forking and thread pools cheaper still. But past a certain connection count, the context switching between all those threads becomes a real cost of its own. The architecture that finally won serves many connections from a single thread driven by an asynchronous event loop , one such worker per core — nginx proved it in practice and laid the C10k problem to rest. (Go reaches the same substrate from another direction — goroutines multiplexed over a netpoller — hiding the loop instead of exposing it.)
And what does a single-threaded event loop remind us of? JavaScript — that is precisely its execution model, forced on it by the browser. Node.js pointed that model at the server, where it happened to match the most efficient architecture we had found, almost exactly. That is much of why it caught on. Python had the same event-loop style in Twisted years before; Tornado pointed it at web serving, and comparable frameworks followed for other languages. The language that won the server was not the fast one — it was the one whose execution model already matched the shape of the work.
The economics reinforce it. Running our own server in the 90s, we had already paid for the hardware and it sat mostly idle — 6% CPU instead of 3% cost us nothing. Today the big cloud providers care a great deal about CPU: it is a finite, metered resource with a real energy and ecological footprint, and they bill for it. Yet for most end users that bill is still minor next to the cost of the people writing the code.
There is an irony in calling the winner interpreted: modern JavaScript engines are tiered JIT machines — an interpreter backed by optimizing compilers — and the lineage is direct. V8 was led by Lars Bak , the engineer behind HotSpot , which had carried adaptive optimization from the Self research VMs into Java. The language that won the server carries the JVM’s own hot-path machinery inside it.
CPU bound?
There is a second reason, and it holds even when the work is CPU bound. Coming from C++, I was puzzled to see Python, Ruby, and JavaScript running serious production systems. So I once profiled one of those Python applications, and found that the Python itself accounted for something like 3% of the running time — the other 97% was spent inside a system library written in C. The Python was glue.
That pattern has only grown. Python now runs plenty of heavy, CPU-bound numerical work, not because the interpreter got fast but because the arithmetic happens in NumPy , pandas , and their kin — compiled, vectorized native code under a thin scripting layer. The clearest case today is AI: Python is the premier language for neural networks, yet it does almost none of the arithmetic itself — PyTorch and TensorFlow hand the tensor math to compiled C++ and GPU kernels through CUDA. It is the same trick as the stored procedure and the JIT: keep the flexible, productive language on top, and let a precompiled core do the expensive part. Once most of the cycles already run in optimized native code, the language wrapped around them barely matters.
CPython itself is joining the pattern late — a specializing adaptive interpreter in 3.11, an experimental JIT in 3.13 — a decade after the native libraries had solved the problem from below. And the traffic ran the other way too: Jython and JRuby simply parked their languages on the JVM and inherited HotSpot’s JIT wholesale.
When the 3% matters
Twice above I waved the CPU bill away — it barely moves an I/O-bound total, and it is minor next to salaries. Both dismissals have a boundary: somebody’s CPU bill is always big enough. Facebook hit it and compiled PHP — first to C++ with HipHop , then to a JIT with HHVM and its own language, Hack. Twitter hit it and moved from Ruby to the JVM. Google poured two decades into V8. AWS designed Graviton around performance per watt. None of them stopped writing the flexible layer — they compiled underneath it. At scale my original arithmetic reasserts itself: when each transaction consumes half the CPU, the same hosts pack twice the workload — fewer machines, smaller datacenters, less power.
And we are paying the piper now. In 2026 we don’t have enough power , enough water , or enough places to build datacenters , while the demand for computing keeps growing.
I see it in component prices. A price moving over time is normal, and usually the item moves with it: often we cannot buy the same thing twice — the new one is different, improved; progress. An unchanged item normally goes the other way and gets cheaper, because the technology behind it is constantly optimized. Statisticians even bake this expectation into price indices: for computers and televisions, hedonic adjustment books a quality improvement as a price decline — the same money buying a better machine counts as things getting cheaper. GPUs are genuinely getting more complex, so their prices have an excuse; CPUs, memory, and disks are the same technology they were a few years ago. Yet three years ago I bought a 20 TB NAS drive for my RAID for about $360, and today the same model — same technical features, nothing improved — lists at twice what I paid. Its 24 TB successor is no explanation either — it is a fifth bigger, not twice. The product didn’t change; the demand did. One way or another, we will all pay more for computing resources. (Incidentally, that cuts against the assumption that AI gets ever cheaper. The total bill follows demand for scarce resources: power, silicon, datacenter space. The demand keeps growing, and replacing people with AI trades salaries for a resource bill that is more likely to rise than shrink.)
Summary
We started with a clean first-principles answer: interpret on the client, compile on the server. The client half held; the server went the other way — interpreted and JITted languages run it too. Server work is mostly I/O bound, so the CPU efficiency we were optimizing was largely free to give away — and where the work really is CPU bound, the heavy lifting usually runs in a precompiled native core anyway. Either way, the factors that actually decide — productivity, portability, ecosystem — chose the managed languages. The same criterion decided both halves of the story: applets died because they could not start fast enough; interpreted servers thrived because they were fast enough where it mattered.
The prediction was mine, and history proved me wrong — but not about the arithmetic, which stands, and not about the problems. The remedies the industry shipped since — plan caches and stored procedures, install-time and idle-time compilers, startup snapshots, native cores under scripting layers — address problems the original analysis named. History rejected the default I picked, not the pressures I saw.
Look closer and the same pattern turns up everywhere: interpret for flexibility and reach, precompile the hot path for speed. The JVM, Android’s runtime, the database engine, and NumPy under Python all walked to that middle ground from different directions. That they converge on the middle is the common case, not a law — the deployment model decides each one’s place on the spectrum, which is why iOS, free to compile everything ahead of time, sits out at the pure-AOT end. That middle ground has a catch of its own, though: it pays off only when the seam between the flexible layer and the compiled core is cheap to cross — and often it isn’t. That seam is a subject of its own: what a boundary crossing actually costs, and what a design turns into once crossing gets expensive. A post on it is on the way.