The runtime
Royal Rumble, every language is a weight class.
A language is a spec — text that promises what map or async mean. A runtime is whatever is actually there at the moment your code executes: translating source to instructions, managing memory, scheduling work, and binding to the world outside the CPU. Many languages have several. Click any card.
Four questions every runtime must answer.
The personality of any runtime is mostly determined by how it resolves these four. Hold them in mind as you read the rest — they're what makes Bun feel different from Node, or HotSpot different from GraalVM.
How does source become CPU work?
Interpret bytecode directly, JIT-compile hot paths at runtime, or AOT-compile everything before shipping. Each is a different gamble on startup time vs. peak throughput.
Who decides when to free?
Tracing GC, reference counting, ownership-based, or manual. Determines latency profile, fragmentation, and whether you ever say the words "stop the world."
How is work scheduled?
OS threads, green threads, goroutines, fibers, event loops, actors. Where the parallelism comes from and what synchronisation costs.
What slice of the world is reachable?
Filesystem, sockets, DOM, GPU, kernel-side hooks, edge KV stores. The runtime decides what the language can do, far more than the language itself.
JavaScript / TypeScript
ECMAScript deliberately defines very little: Array, Promise, async/await. Everything else — fetch, setTimeout, fs.readFile, document — is provided by the host. That's why the same language behaves so differently in a browser vs. a server vs. an edge worker. Each is a distinct ambient world bolted onto an engine.
Python
Python the language is one thing; Python the runtime is a choice. CPython is the reference and what most people use; PyPy is the same language with a JIT instead of a bytecode interpreter; MicroPython is the same language with the standard library mostly torn out so it can fit on a microcontroller. The differences are dramatic — performance, threading semantics, C-extension compatibility all flip depending on which one you boot.
JVM bytecode — Java, Kotlin, Scala, Clojure
Unusually, the runtime here is shared by an entire family of languages. They all compile to JVM bytecode, then a JVM executes it. So Kotlin's pauses, Scala's reflection, and Clojure's startup time are really HotSpot's pauses, HotSpot's reflection, HotSpot's startup. Picking GraalVM or OpenJ9 changes all of those without touching a line of source.
.NET / CIL — C#, F#, VB.NET
The Microsoft counterpart to the JVM. Source compiles to CIL (Common Intermediate Language), and a CLR executes it. Modern .NET has unified what used to be three forks: CoreCLR for server, Mono for mobile and games, and NativeAOT for trim-down deployments where you want zero JIT. Same language surface, three different runtime experiences.
Go
Go has effectively one runtime, but it's an unusual one: it's compiled into every binary. No VM to install, no JIT to warm up. Yet that statically-linked binary contains a goroutine scheduler, a concurrent GC, channels, and a network poller — all running invisibly underneath your main(). The pitch was "deploy a single file"; the design consequence is that every Go program ships with a few megabytes of runtime even for hello-world.
Rust
Rust is the clarifying counter-example. People say it has "no runtime" because nothing tracks references at execution time and no scheduler is mandatory — ownership is checked at compile time and goes away before the program runs. But that's only true for synchronous Rust. The moment you write async fn, you need a runtime to drive the futures, and you pick one. Tokio, async-std, smol — same language, three different worlds.
BEAM — Erlang, Elixir, Gleam
Most runtimes start from "execute the language." BEAM started from "build a phone switch that never goes down." Everything else — millions of lightweight processes, per-process GC, hot code reloading, transparent network distribution — falls out of that founding constraint. Three languages now ride on it; they share the runtime's superpowers and its quirks (no shared memory, weird-looking pattern matching at every boundary).
Ruby
Ruby spent a decade as a pure interpreter (MRI), then YARV gave it a real bytecode VM, then YJIT gave it a JIT. Each step roughly halved the gap to V8. TruffleRuby — Ruby on GraalVM's polyglot stack — closes the rest in some benchmarks. It's a useful demonstration that "the language is slow" is almost always shorthand for "the dominant runtime hasn't caught up yet."
Lua
Lua exists because the original authors needed a tiny scripting language they could embed in other programs. That constraint produced a runtime that fits in a few hundred kilobytes — and which now lives inside Nginx, Redis, World of Warcraft, Roblox, and Neovim. Then Mike Pall wrote LuaJIT and turned this small embeddable thing into one of the fastest dynamic-language implementations ever built. Same language, two extraordinarily different engines.
WebAssembly — Rust, C, Go, Zig, AssemblyScript
Wasm isn't a language people write directly — it's a binary instruction format that other languages compile to. The interesting thing is that this turns "runtime" into a commodity: any host that implements the Wasm spec can run the same bytecode. The browser was first; standalone runtimes (Wasmtime, Wasmer) followed; edge platforms (Fastly, Fermyon) bet the whole product on it. Cold starts in microseconds because the verifier replaces the trust model that VMs usually need.