Server-Side Execution Models

How web servers run dynamic code: CGI, modules, application servers, and serverless

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:

  1. Forks a child process
  2. Sets up environment variables (QUERY_STRING, REQUEST_METHOD, etc.)
  3. Execs the program (replacing the child with the script)
  4. Captures stdout as the HTTP response
  5. 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.