AlgoInsight

Free lesson · Node.js Deep Dive

Before Node: JavaScript in the Browser, Server-Side Stagnation

Easy9 min readnodejsjavascriptbackend

Before Node: JavaScript in the Browser, Server-Side Stagnation

To understand why Node.js exists, you have to understand what the web looked like in 2008.

Not 2018. Not 2013. 2008. Before Ryan Dahl announced Node at JSConf EU in November 2009, the world of server-side development looked nothing like what you know today. JavaScript was a browser-only language. The serious server-side languages were PHP, Java, Ruby, Python, and Perl. Most production web servers were either Apache HTTP Server or Microsoft IIS. Real-time anything on the web was so painful that developers had basically given up on it; you faked it with polling. The phrase "full-stack JavaScript" would have been treated as a joke.

This lesson covers what that world looked like, why it had the problems it had, and what specifically those problems were. The next lesson - 1.3 - covers what Ryan Dahl proposed to do about them.


JavaScript Was a Browser Language

JavaScript was created in 1995 by Brendan Eich at Netscape, in famously about ten days. It was designed to make web pages interactive - popping up alert dialogs, validating form inputs, animating buttons. Nobody at Netscape thought it would be a serious programming language. The internal codename was "Mocha," then "LiveScript," then "JavaScript" - the last one chosen for marketing reasons, because Java was the hot language of 1995 and Netscape's marketing team wanted to ride the wave.

For the next fourteen years, JavaScript stayed in the browser. There were a few academic and hobbyist experiments to run it server-side - Netscape had a product called Netscape Enterprise Server with server-side JavaScript in 1996, and Mozilla had Rhino, a JavaScript engine in Java, but neither caught on. The dominant server-side languages were:

Notice the absence of JavaScript on that list. There was no equivalent. The closest thing to "JavaScript on the server" was generating JavaScript from a server-side language - your PHP file would echo out JavaScript that ran in the browser. The language was strictly client-side.


The Apache Concurrency Model

To understand the specific problem Node was designed to solve, you need to understand how Apache HTTP Server handled requests in 2008.

Apache's default model - called the Prefork MPM (Multi-Processing Module) - worked like this: when Apache started, it spawned a configurable number of worker processes. Each worker process could handle one HTTP request at a time. When a request arrived, the operating system handed it to an idle worker. That worker did the work - read the request, executed the PHP script, queried MySQL, generated the response, wrote it back. While the worker was doing this, it could not handle any other requests.

flowchart LR
    Internet[Internet] --> Listener[Apache listener]
    Listener --> W1[Worker 1<br/>handling request A]
    Listener --> W2[Worker 2<br/>handling request B]
    Listener --> W3[Worker 3<br/>handling request C]
    Listener --> W4[Worker 4<br/>idle]
    W1 --> DB1[(MySQL)]
    W2 --> DB1
    W3 --> DB1
A flowchart showing the Apache prefork model. The internet sends requests to a single Apache listener, which distributes them to a pool of worker processes. Each worker handles exactly one request at a time, blocking on operations like database queries. When all workers are busy, additional requests wait or get rejected.

If your Apache was configured with 256 workers, you could handle 256 concurrent requests. The 257th request would queue, or get rejected.

This model has nice properties. It's simple. Each request gets a fresh worker with isolated state. A crashing request kills one worker and Apache spawns a replacement. Memory leaks are limited to a single worker's lifetime. PHP programmers loved it because their mental model could be "the script runs, the script ends, everything resets," which is dramatically simpler than reasoning about long-running shared state.

The problem is that it doesn't scale to the kind of workloads that started appearing around 2008.


The C10K Problem

In 1999, a developer named Dan Kegel wrote an article titled "The C10K problem." The premise: as the web grew, web servers needed to handle 10,000 concurrent connections per machine - and Apache's process-per-request model simply could not. The math didn't work. If each Apache process used 5 MB of memory (a reasonable number, including the PHP interpreter), then 10,000 workers needed 50 GB of RAM. In 2008, a top-of-the-line server had 16-32 GB. You couldn't even start 10,000 workers.

But the deeper problem wasn't memory. It was that most of those 10,000 connections were idle most of the time. They were waiting for something - waiting for a database query to return, waiting for a file to read from disk, waiting for a slow client to upload the rest of a form. While they waited, a worker was tied up, doing nothing.

gantt
    title What a typical Apache worker does in 100ms
    dateFormat X
    axisFormat %L

    section Worker 1
    Read request   :a1, 0, 5
    Parse headers  :a2, after a1, 2
    Query database (WAITING) :crit, a3, after a2, 50
    Render template :a4, after a3, 20
    Write response :a5, after a4, 5
    Idle (block)   :a6, after a5, 18
A Gantt chart showing what a typical Apache worker does over a single 100 millisecond request. About 5 milliseconds reading the request, 2 milliseconds parsing headers, then 50 milliseconds blocked waiting for the database query to return, then 20 milliseconds rendering the template, 5 milliseconds writing the response, and 18 milliseconds idle. Out of 100 milliseconds, about 70% of the worker's time is either blocked on I/O or idle.

Look at that breakdown. About 70% of the worker's time is not doing CPU work. It's blocked, waiting for something to happen. The CPU sat idle. The worker was tied up. Multiply by 256 workers and you've got an absurd amount of compute wasted on waiting.

This was the wasteful pattern Apache was built around. And it was fine in 1996 when websites served HTML and that was it. By 2008, with AJAX, real-time chat features, file uploads, long-running database queries, and external API calls, the model was creaking.

The "C10K" framing was rhetorical, but the underlying point was real: web servers needed to handle massive concurrency efficiently, and the prefork model could not.


What Already Existed (and Why It Wasn't Enough)

Plenty of smart engineers had already noticed this problem. Solutions existed.

Threads instead of processes. Apache had a Worker MPM mode that used threads instead of forked processes. Threads are cheaper than processes - a thread might use 1 MB instead of 5 MB. You could maybe get to 5,000-10,000 threads. But threads share memory, which means thread-safety bugs, and the underlying issue (threads blocking on I/O while idle) doesn't go away.

Nginx. Released in 2004 by Igor Sysoev, Nginx was a web server explicitly designed around the C10K problem. It used a small fixed number of worker processes, each running an event loop that could handle thousands of connections concurrently by never blocking. Nginx was fast. By 2008 it was gaining traction as a reverse proxy and static file server.

But Nginx was a web server, not an application server. It served static files and proxied to backends. The slow PHP work still happened in the slow PHP backend, which was still Apache + mod_php most of the time. Nginx solved part of the problem and pushed the rest somewhere else.

Java NIO and Netty. Java introduced non-blocking I/O in 2002 (Java NIO), and the Netty framework from 2004 onward built a high-performance event-loop-based server framework on top. This worked. It still does - large chunks of high-performance Java infrastructure today (Cassandra, Elasticsearch, Hadoop) use Netty. But writing application code on top of NIO and Netty was brutal. You had to think in callbacks, write state machines explicitly, and deal with Java's lack of closures. Almost nobody wrote application code at the NIO level directly.

Twisted (Python) and EventMachine (Ruby). Both were event-loop frameworks in their respective languages. Both worked. Both had small but devoted communities. Neither caught on with the mainstream of their language.

The pattern, looking back: event-driven non-blocking I/O worked. Multiple people had built it. The performance gains were real. But every existing implementation was awkward to use, because the underlying language wasn't designed for it. Java's callbacks were verbose. Python's coroutines were primitive in 2008. Ruby's EventMachine had a reputation for being hard to debug.

What you wanted was a language where async-by-default felt natural - where callbacks were already idiomatic, where the runtime was small and embeddable, where the development experience was good.

There was a language like that. It was already known to virtually every web developer alive. It was JavaScript.


V8 Changes the Math

The other piece that has to come together for Node to be possible is a fast JavaScript engine you can embed.

For most of JavaScript's life, JavaScript engines were slow and tightly coupled to specific browsers. Mozilla had SpiderMonkey. Microsoft had Chakra. WebKit had JavaScriptCore. None of them was particularly fast, and none of them was designed to be embedded outside the browser.

Then in 2008, Google released V8. V8 was the JavaScript engine inside Chrome, and it was several times faster than any existing JavaScript engine. It used aggressive JIT compilation - compiling JavaScript directly to machine code, with optimizations that approached the speed of compiled languages for common workloads. Crucially, V8 was open source, designed to be embeddable, and released under a permissive license.

For the first time, you could take a fast JavaScript engine and put it inside something other than a browser. That something could be a server, a desktop app, a build tool, a CLI utility - anything you wanted.


The Pieces on the Table

By mid-2009, all the pieces existed:

What was missing was someone to assemble them. The historical accident of who that someone was, and what specifically they assembled, is Lesson 1.3.


What I Want You to Take From This Lesson

Two things.

One: Node.js didn't invent non-blocking I/O. This is a thing I see misrepresented constantly online. Node didn't invent the event loop. Node didn't invent async programming. The event-loop, non-blocking-I/O pattern existed in production for years before Node. What Node did was combine an existing concurrency model with a fast JavaScript engine and a programming language that didn't make async miserable to write. That combination was new and that combination is why Node won.

Two: The problem Node was solving was specific. It wasn't "make servers fast in general." It was "let one process handle thousands of concurrent connections that are mostly idle." If your workload doesn't look like that - say you're doing heavy CPU work like video encoding, or scientific computation, or compiler work - Node's design doesn't help you, and might actually hurt. We'll come back to this in Lesson 1.4 and again in Module 14.

The next lesson is the centerpiece of this module: Ryan Dahl's 2009 talk, what he said, and why he was right.


Try It

Exercise: Warmup (10 minutes). Watch the first 5 minutes of Igor Sysoev's Nginx introduction talk from 2008. You don't need to follow every detail - just listen for what problem he says he was trying to solve. The problem is the same one Ryan Dahl describes a year later, in different words.

Exercise: Core (20 minutes). Read Dan Kegel's original C10K article. It's old (1999), but the framing holds up and the problem it names is the one Node was eventually built to solve. You don't need to understand every detail of the I/O strategies he discusses - Modules 3 and 4 of this course will cover them. Just read it for the framing.

Exercise: Stretch (45 minutes). Set up an Apache server on your laptop (or a local Linux VM). Configure it with mod_php. Write a single PHP script that does sleep(5); echo "Hello"; - simulating a slow database query. Then use a tool like hey or ab (ApacheBench) to fire 100 concurrent requests at it. Watch what happens. If your Apache is configured with the default 25 prefork workers, the 26th-100th requests will queue. With the slow script, total wall time will be roughly (100 / 25) × 5 = 20 seconds for all 100 requests. This is exactly the workload Node was designed for - the same 100 requests on a Node server doing the same simulated wait completes in just over 5 seconds total. We'll prove this in Module 5.


What's Next

Lesson 1.3 is the centerpiece of Module 1: Ryan Dahl's November 2009 JSConf EU talk announcing Node.js. We'll watch the original (it's still online), examine what he proposed, look at how the actual Node implementation matched his stated goals, and reflect on what he got right and what he got wrong with fifteen years of hindsight.

Back to Node.js Deep Dive