AboutBlogContact
Backend EngineeringJune 14, 2002 7 min read 120Updated: June 22, 2026

LAMP: The Stack That Won the Web (2002)

AunimedaAunimeda
📋 Table of Contents

In 2002, a question that every developer building a web application had to answer was: what do I run this on? The enterprise answer was Windows 2000 Server + IIS + SQL Server + ASP - expensive, license-heavy, requiring Windows expertise. The open-source answer was LAMP: Linux, Apache, MySQL, PHP.

LAMP was not a product. Nobody invented it. It emerged organically as the cheapest, most deployable combination of open-source tools that worked together reliably. By 2002 it was not just viable - it was dominant for new web projects. Shared hosting plans worldwide ran Apache on Linux. MySQL was included free. PHP came bundled. You could deploy a working web application for $8 a month.

This changed everything about who could build on the web.


The Components in 2002

Linux (Red Hat 7.3 / Debian 3.0): The OS. Free. Stable. Ran Apache without a Windows license. Kernel 2.4 was the production standard.

Apache 1.3 / 2.0: The web server. mod_php loaded PHP as an Apache module - no CGI spawning, no process per request. PHP ran in-process, dramatically faster than Perl CGI.

MySQL 3.23 / 4.0: The database. No transactions in 3.23 (MyISAM only). MySQL 4.0 (2002) added InnoDB with transactions as a stable option. Fast for reads. Good enough for most web applications.

PHP 4.3: The language. register_globals was on by default (a security disaster that took years to kill). Procedural code embedded in HTML. Fast to write, fast to deploy.


A Real 2002 PHP + MySQL Application

<?php
// config.php - database connection, 2002 style
// No PDO (not until PHP 5.1). No MySQLi (PHP 4 only had mysql_* functions).

define('DB_HOST', 'localhost');
define('DB_USER', 'webapp');
define('DB_PASS', 'secret123');
define('DB_NAME', 'myapp');

$conn = mysql_connect(DB_HOST, DB_USER, DB_PASS);
if (!$conn) {
    die('Database connection failed: ' . mysql_error());
}
mysql_select_db(DB_NAME, $conn);
mysql_query("SET NAMES 'utf8'", $conn);
?>
<?php
// products.php - product listing page
require_once 'config.php';
require_once 'session.php';

$category_id = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$page        = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$per_page    = 20;
$offset      = ($page - 1) * $per_page;

// In 2002, most developers concatenated SQL directly.
// The correct approach: cast to int (we did, above) to prevent injection.
if ($category_id > 0) {
    $where = "WHERE category_id = $category_id AND active = 1";
} else {
    $where = "WHERE active = 1";
}

$total_result = mysql_query(
    "SELECT COUNT(*) AS total FROM products $where",
    $conn
);
$total_row   = mysql_fetch_assoc($total_result);
$total_pages = ceil($total_row['total'] / $per_page);

$result = mysql_query(
    "SELECT id, name, price, image, description
     FROM products
     $where
     ORDER BY created_at DESC
     LIMIT $offset, $per_page",
    $conn
);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Products - My Store</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" type="text/css" href="/css/style.css" />
</head>
<body>
<?php include 'header.php'; ?>

<div id="content">
  <h1>Products</h1>
  
  <?php if (mysql_num_rows($result) === 0): ?>
    <p>No products found.</p>
  <?php else: ?>
    <table cellpadding="8" cellspacing="0" border="0" class="product-table">
      <tr>
        <th>Image</th>
        <th>Product</th>
        <th>Price</th>
        <th>Action</th>
      </tr>
      <?php while ($row = mysql_fetch_assoc($result)): ?>
      <tr class="<?php echo ($i++ % 2 === 0) ? 'even' : 'odd'; ?>">
        <td>
          <img src="/images/products/<?php echo htmlspecialchars($row['image']); ?>"
               width="60" height="60" alt="<?php echo htmlspecialchars($row['name']); ?>" />
        </td>
        <td>
          <a href="product.php?id=<?php echo $row['id']; ?>">
            <?php echo htmlspecialchars($row['name']); ?>
          </a>
          <br />
          <small><?php echo htmlspecialchars(substr($row['description'], 0, 80)); ?>...</small>
        </td>
        <td>$<?php echo number_format($row['price'], 2); ?></td>
        <td>
          <a href="cart.php?action=add&id=<?php echo $row['id']; ?>">Add to Cart</a>
        </td>
      </tr>
      <?php endwhile; ?>
    </table>
    
    <!-- Pagination - hand-built, no framework -->
    <div class="pagination">
      <?php for ($p = 1; $p <= $total_pages; $p++): ?>
        <?php if ($p == $page): ?>
          <strong><?php echo $p; ?></strong>
        <?php else: ?>
          <a href="?cat=<?php echo $category_id; ?>&page=<?php echo $p; ?>"><?php echo $p; ?></a>
        <?php endif; ?>
      <?php endfor; ?>
    </div>
  <?php endif; ?>
</div>

<?php include 'footer.php'; ?>
</body>
</html>

The MySQL Schema (2002)

-- MySQL 4.0, MyISAM engine (InnoDB was optional, not default)
-- Full-text search built into MyISAM - one real advantage over InnoDB at the time

CREATE TABLE categories (
    id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name        VARCHAR(100) NOT NULL,
    slug        VARCHAR(100) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY (slug)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE products (
    id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
    category_id INT UNSIGNED NOT NULL,
    name        VARCHAR(200) NOT NULL,
    description TEXT,
    price       DECIMAL(10,2) NOT NULL DEFAULT 0.00,
    image       VARCHAR(255),
    active      TINYINT(1) NOT NULL DEFAULT 1,
    created_at  DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
    PRIMARY KEY (id),
    KEY         idx_category (category_id),
    KEY         idx_active (active),
    FULLTEXT     idx_search (name, description)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- Session table - storing PHP sessions in MySQL (not files)
-- For multi-server setups or when /tmp wasn't shared
CREATE TABLE sessions (
    session_id  VARCHAR(32) NOT NULL,
    data        TEXT,
    last_access INT UNSIGNED NOT NULL,
    PRIMARY KEY (session_id)
) ENGINE=MyISAM;

Apache Configuration: .htaccess

Every LAMP developer in 2002 knew .htaccess by heart:

# .htaccess - the 2002 developer's Swiss Army knife

# URL rewriting: /product/42 → /product.php?id=42
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^product/([0-9]+)/?$ product.php?id=$1 [L,QSA]
RewriteRule ^category/([a-z0-9-]+)/?$ products.php?slug=$1 [L,QSA]

# PHP configuration overrides (shared hosting didn't give php.ini access)
php_flag  register_globals  Off   # critical security fix
php_flag  display_errors    Off   # never on production
php_value upload_max_filesize 5M
php_value post_max_size       6M

# Prevent directory listing
Options -Indexes

# Protect config files
<FilesMatch "^(config|db)\.php$">
    Order allow,deny
    Deny from all
</FilesMatch>

# Cache static assets
<FilesMatch "\.(jpg|jpeg|gif|png|css|js)$">
    ExpiresActive On
    ExpiresDefault "access plus 7 days"
</FilesMatch>

What LAMP Got Right

Zero licensing cost. A complete production web stack cost nothing in software licenses. The only cost was hardware or hosting. This made the web accessible to individuals, startups, and developing-world developers in a way that Windows/IIS could not.

Shared hosting compatibility. Every shared hosting provider in 2002 ran Apache on Linux with PHP and MySQL. You could develop on a $8/month plan, test there, and move to a dedicated server when you outgrew it. The stack was identical everywhere.

mod_php performance. PHP running inside Apache as a module was dramatically faster than CGI. No process spawning per request. The interpreter stayed resident in memory. This was good enough for millions of page views per day on modest hardware.


What LAMP Got Wrong

register_globals. PHP 4's register_globals = On by default meant GET/POST parameters were automatically available as global variables. $id was set if ?id=5 was in the URL. This made injection attacks trivial. Turning it off was the first security fix any competent developer applied - but shared hosting often couldn't be configured, and developers who didn't know any better left it on.

No transactions by default. MyISAM had no transactions. A partial insert during a crash left the database inconsistent. Most 2002 LAMP applications were not using InnoDB and were not thinking about this. Order tables with no transaction guarantees were a real problem.

No prepared statements. The mysql_* extension had no prepared statement support. SQL injection protection required careful manual escaping with mysql_real_escape_string() - which many developers forgot, misapplied, or had never heard of.

The LAMP stack won the web despite these flaws, not because it was architecturally superior. It won because it was free, everywhere, and good enough. The problems it had were solved over the next decade - PDO in PHP 5.1, InnoDB as default in MySQL 5.5, register_globals deprecated in PHP 5.3 and removed in PHP 5.4. The platform matured around its weaknesses.

By 2005 it was the most deployed web stack on earth. By 2010 it was the foundation of WordPress, Drupal, Joomla, MediaWiki - the software running most of the web's content.


Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.

Contact us for backend engineering services. See also: Custom Software Development, Web Development

Read Also

How to Use Redis for Caching in PHP: Cutting Response Times from 800ms to 40ms (2015)aunimeda
Backend Engineering

How to Use Redis for Caching in PHP: Cutting Response Times from 800ms to 40ms (2015)

Redis caching reduced our PHP app's average response time from 800ms to 40ms on product listing pages. The pattern: cache database query results with TTL, invalidate on write. Here's the exact implementation with cache key strategy, stampede prevention, and cache warming.

How to Build a REST API with Laravel 5.0 (2015)aunimeda
Backend Engineering

How to Build a REST API with Laravel 5.0 (2015)

Laravel 5.0 launched in February 2015 with a redesigned folder structure and form requests. It became the fastest way to build a REST API in PHP. Here's the full pattern: routes, controllers, Eloquent serialization, authentication middleware, and consistent error responses.

Building a Proper REST API in PHP: Lessons From Our First Mobile Backendaunimeda
Backend Engineering

Building a Proper REST API in PHP: Lessons From Our First Mobile Backend

When we built our first iOS app in 2013, we designed the backend API by feel. Here's what we got wrong, what we fixed, and the principles that still hold up today.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles