The Challenge
Web servers were originally designed to serve static files. When a browser requests /about.html, the server finds that file and sends it back. Simple.
But what about dynamic content? Shopping carts, user authentication, database queries? The server needs to run code to generate responses. The question is: how does that code execute?
Static File Serving:
┌─────────┐ GET /page.html ┌─────────┐ read file ┌──────────┐
│ Browser │ ───────────────────────▶│ Apache │ ─────────────────▶│ page.html│
│ │ ◀─────────────────────── │ │ ◀───────────────── │ │
└─────────┘ HTML response └─────────┘ file contents └──────────┘
Dynamic Content (the question):
┌─────────┐ GET /cart.php ┌─────────┐ ??? ┌──────────┐
│ Browser │ ───────────────────────▶│ Apache │ ─────────────────▶│ PHP Code │
│ │ ◀─────────────────────── │ │ ◀───────────────── │ │
└─────────┘ HTML response └─────────┘ HTML output └──────────┘
How does this happen?
The Execution Models
Over the decades, several approaches have emerged. Each makes different trade-offs between simplicity, performance, and resource usage.
| Model | How It Works | Examples |
|---|---|---|
| Fork-Exec CGI | Server spawns a new process for each request | C programs, Perl scripts, early web |
| Server Module | Interpreter embedded in the server process | mod_php, mod_perl |
| Application Server | Persistent process handles many requests | Node.js, Python WSGI/ASGI, Java Servlets |
| Serverless/FaaS | Cloud platform manages execution | AWS Lambda, Cloudflare Workers |
Fork-Exec CGI (Common Gateway Interface)
The original (1993) solution. When a request arrives for a CGI script, the web server:
- Forks a child process
- Sets up environment variables (QUERY_STRING, REQUEST_METHOD, etc.)
- Execs the program (replacing the child with the script)
- Captures stdout as the HTTP response
- Process exits when done
CGI Execution Flow:
┌─────────────────────────────────────────┐
Request 1 ──▶ Apache ──▶ fork() ──▶│ Child Process │
│ exec("/cgi-bin/script.pl") │
│ Read ENV: QUERY_STRING, REQUEST_METHOD │
│ Execute script logic │
│ Print HTTP headers + body to stdout │
│ exit(0) │
└─────────────────────────────────────────┘
│
▼
Response to client
Request 2 ──▶ Apache ──▶ fork() ──▶ [New process, starts fresh]
Request 3 ──▶ Apache ──▶ fork() ──▶ [New process, starts fresh]
Server Module (Embedded Interpreter)
Instead of forking a new process, the interpreter runs inside the web server. Apache's mod_php is the classic example.
Module Execution (mod_php): ┌──────────────────────────────────────────────────────────┐ │ Apache Process │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ mod_php (embedded PHP) │ │ │ │ │ │ │ │ Request 1 ──▶ Parse cart.php ──▶ Execute ──▶ Output│ │ │ │ Request 2 ──▶ Parse user.php ──▶ Execute ──▶ Output│ │ │ │ Request 3 ──▶ Parse cart.php ──▶ Execute ──▶ Output│ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ (Same process handles many requests, no fork overhead) │ └──────────────────────────────────────────────────────────┘
Application Server (Persistent Process)
The application runs as its own long-lived process. The web server (or load balancer) proxies requests to it.
Application Server (Node.js example):
┌─────────┐ ┌─────────┐ ┌─────────────────────────────┐
│ Nginx │ ──────── │ Proxy │ ──────── │ Node.js Process │
│ (port │ forward │ to │ │ │
│ 80) │ ──────▶ │ :3000 │ ──────▶ │ const app = express(); │
└─────────┘ └─────────┘ │ app.get('/', (req, res) => │
│ res.send('Hello'); │
│ }); │
│ app.listen(3000); │
│ │
│ [Runs continuously] │
│ [Handles many requests] │
│ [Maintains state in memory]│
└─────────────────────────────┘
Model Comparison
| Aspect | CGI | Module (mod_php) | App Server (Node) |
|---|---|---|---|
| Startup per request | Full process | None | None |
| Memory isolation | Complete | Partial | None |
| State between requests | Impossible | Limited | Easy (in-memory) |
| Requests per second | Low (~100s) | High (~1000s) | Very High (~10,000s) |
| Crash impact | One request | One Apache worker | All requests |
| Deployment | Drop files | Drop files | Process manager |
Historical Context
The evolution of server-side execution reflects the web's growth:
- 1993: CGI specification published. Perl becomes the "duct tape of the internet."
- 1995: PHP created, initially as CGI scripts, later as Apache module.
- 1997: mod_perl brings Perl into Apache's process space.
- 2009: Node.js introduces event-driven JavaScript server.
- 2014: AWS Lambda launches, popularizing serverless.
Each model didn't replace the previous one entirely — they coexist based on use case requirements.