AboutBlogContact
Backend EngineeringDecember 5, 2011 7 min read 126Updated: May 18, 2026

Why We Chose Node.js in 2011

AunimedaAunimeda
📋 Table of Contents

Why We Chose Node.js in 2011

In November 2011 we had a specific problem: a business intelligence dashboard that polled the server every 5 seconds from 150+ concurrent users. The PHP backend was generating 1,800+ database queries per minute, most returning identical data. Server load was at 70% during business hours. The solution looked like caching - but the client needed fresh data within 10 seconds of a sale closing.

We could have added Memcached. We could have tuned MySQL. We chose Node.js instead.

This is the exact reasoning we used, including the objections we had internally.


The Technical Problem First

Our dashboard was a polling loop:

// Client side (jQuery) - what we had
setInterval(function() {
  $.get('/api/dashboard-data', function(data) {
    updateCharts(data);
  });
}, 5000);

150 users × polling every 5 seconds = 30 requests/second to the PHP backend. Each request:

  1. PHP-FPM spawns/reuses a worker
  2. Worker connects to MySQL
  3. Runs 4-6 queries (daily totals, hourly breakdown, recent orders, agent stats)
  4. Returns JSON
  5. Worker freed

The queries themselves were fast - under 20ms each. The overhead was PHP-FPM process management + MySQL connection overhead × 30 requests/second.

The data on the dashboard changed when a sale was closed - roughly 20-40 times per hour during business hours. So 98% of those 1,800 queries per minute returned the same data that had been returned 5 seconds earlier.

The fundamental mismatch: a pull-based model for data that only changed 30-40 times per hour.


Why Node.js Was the Right Fit

We'd been watching Node.js since Ryan Dahl's 2009 JSConf talk. In November 2011, it was version 0.6.x - the first LTS-designated release, though that term didn't exist yet. The ecosystem was thin. npm had around 6,000 packages.

The case for Node.js for this specific problem was based on two things:

1. WebSockets made the architecture correct.

Instead of 150 clients asking "is there new data?" every 5 seconds, the server would tell clients when new data was available. Push instead of pull. WebSockets provided a persistent connection; Socket.io 0.9 (the library we used) handled browser compatibility, reconnection, and fallbacks.

The new architecture:

  • One WebSocket connection per user: 150 connections total
  • Server sends update to all clients when data changes: 30-40 times/hour
  • MySQL queries: 30-40 per hour instead of 1,800 per minute

The ratio improvement: 2,700x fewer database queries under real load.

2. Node.js handles persistent connections efficiently.

150 open WebSocket connections with one process. In Apache/PHP, 150 concurrent connections meant up to 150 PHP workers, each holding 2-8MB of stack. In Node.js, 150 connections were handled by one event loop - sub-megabyte overhead per connection.


The Internal Objections We Had

We were a PHP team. Nobody had shipped Node.js to production. The objections were real:

Objection 1: "Node.js is not production-ready."

Version 0.6 was the first release with a stated stability guarantee. The argument against: even 0.6 had memory leak reports, and the process model (restart if it crashes) was still maturing.

Our response: we'd run Node.js behind nginx, with PM2 (still in beta) as the process manager. If Node crashed, PM2 would restart it within 1-2 seconds. The dashboard going blank for 2 seconds was acceptable for a BI tool. It was not acceptable for checkout.

We were deliberately choosing a low-stakes first deployment: a dashboard, not a transaction endpoint. If Node.js failed us, users would see a blank chart, not lose a purchase.

Objection 2: "Nobody on the team knows Node.js."

True. We allocated one developer (me) to learn it while building. The learning curve was lower than expected: we already knew JavaScript; the new mental model was the event loop and async I/O. Two weeks of nights and weekends reading the docs and building test servers.

The real risk wasn't learning Node.js - it was debugging an unfamiliar runtime in production. We mitigated by over-logging:

// Every significant event logged with timestamps
var winston = require('winston');
var logger = new winston.Logger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: '/var/log/dashboard.log' })
  ]
});

io.on('connection', function(socket) {
  logger.info('Client connected', {
    id: socket.id,
    ip: socket.handshake.address.address,
    timestamp: new Date().toISOString()
  });
  
  socket.on('disconnect', function() {
    logger.info('Client disconnected', { id: socket.id });
  });
});

// Log every broadcast
function broadcastUpdate(section, data) {
  logger.info('Broadcasting update', {
    section: section,
    clientCount: io.sockets.clients(section).length,
    timestamp: new Date().toISOString()
  });
  io.to('dashboard:' + section).emit('update', data);
}

Objection 3: "PHP works fine, why take the risk?"

This was the hardest objection because it wasn't wrong. PHP would have worked. Memcached + PHP + polling at 10 seconds instead of 5 would have reduced load by 50%. That was a 2-day project versus a 3-week Node.js build.

Our answer was forward-looking. We had two other client projects that needed real-time features: a live inventory tracker and a collaborative order management tool. If we validated Node.js here, those projects had a proven runtime. The 3 weeks was an investment in capability, not just a solution to one problem.


The Implementation: What We Actually Built

// server.js - the complete initial implementation
var http = require('http');
var express = require('express');
var io = require('socket.io');
var mysql = require('mysql');

var app = express();
var server = http.createServer(app);
var socketServer = io.listen(server);

// MySQL connection pool
var pool = mysql.createPool({
  host: 'db.internal',
  user: 'dashboard',
  password: process.env.DB_PASSWORD,
  database: 'sales',
  connectionLimit: 5  // Max 5 DB connections; Node handles the queuing
});

// Dashboard data queries
function getDashboardData(section, callback) {
  var queries = {
    'sales': 'SELECT date(created_at) as day, sum(total) as revenue, count(*) as orders FROM orders WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY day ORDER BY day',
    'agents': 'SELECT agent_id, count(*) as closed, sum(total) as revenue FROM orders WHERE date(created_at) = CURDATE() GROUP BY agent_id',
    'recent': 'SELECT id, customer_name, total, status, created_at FROM orders ORDER BY created_at DESC LIMIT 10'
  };
  
  pool.query(queries[section] || queries['sales'], function(err, rows) {
    if (err) return callback(err);
    callback(null, rows);
  });
}

// WebSocket server
socketServer.on('connection', function(socket) {
  socket.on('subscribe', function(section) {
    socket.leaveAll();
    socket.join('dashboard:' + section);
    
    // Send current data immediately on subscribe
    getDashboardData(section, function(err, data) {
      if (!err) socket.emit('update', data);
    });
  });
});

// Poll MySQL for changes - the bridge from push-database to push-client
// In 2011 MySQL had no change notification; we polled every 3 seconds
// but only broadcast when data actually changed
var lastDataHash = {};

setInterval(function() {
  ['sales', 'agents', 'recent'].forEach(function(section) {
    getDashboardData(section, function(err, data) {
      if (err) return;
      
      var hash = JSON.stringify(data);
      if (hash !== lastDataHash[section]) {
        lastDataHash[section] = hash;
        socketServer.to('dashboard:' + section).emit('update', data);
      }
    });
  });
}, 3000);

server.listen(3001);

We still polled MySQL every 3 seconds - we had no MySQL change notification in 2011. But instead of 150 clients polling PHP every 5 seconds (750 PHP requests/minute), we had 1 Node.js process polling MySQL every 3 seconds (3 queries per section × 3 sections = 9 queries/minute) and pushing to all connected clients via WebSockets.

The client side became simple:

// Client - replaced the setInterval polling with Socket.io
var socket = io.connect('http://dashboard.internal:3001');

socket.on('connect', function() {
  socket.emit('subscribe', currentSection);
});

socket.on('update', function(data) {
  updateCharts(data);  // Same function as before
});

socket.on('disconnect', function() {
  showConnectionWarning();
});

The Results After 60 Days

Metric Before (PHP polling) After (Node.js WebSockets)
MySQL queries/hour at 150 users ~108,000 ~540
Server CPU (business hours) 68-74% 8-12%
Dashboard update lag 0-5 seconds <0.5 seconds
Memory (server process) 350-480MB (PHP-FPM workers) 42MB (Node.js)
Incidents in 60 days 0 (no baseline) 1 (Node.js OOM after 18 days)

The one incident: a memory leak in the lastDataHash object. We were pushing new data comparisons into it without clearing old section data. Fixed in 20 minutes, 2-second downtime. PM2 auto-restarted before we were paged.


What We'd Do Differently

We'd still choose Node.js. We'd add:

  • node --max-old-space-size=256 to limit heap before we hit OOM
  • Heap profiling from day one (v8-profiler was available in 2011)
  • A 5-minute PM2 memory restart: max_memory_restart: '200M'
  • Redis for state instead of in-process hash (necessary once we needed multiple Node.js instances)

The decision was right. The execution had a memory leak that took 18 days to surface. Both things are true.

The broader lesson: new technology in production is always riskier than you estimate. The mitigation isn't to avoid it - it's to choose low-stakes first deployments, over-instrument, and have a rollback path. We had all three. The incident was manageable.


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

Node.js + TypeScript: Building a Production REST API from Scratch in 2026aunimeda
Backend Engineering

Node.js + TypeScript: Building a Production REST API from Scratch in 2026

A complete guide to building a production-ready REST API with Node.js and TypeScript - authentication, validation, error handling, rate limiting, logging, and deployment. No shortcuts.

PostgreSQL Performance Optimization: The Practical Guide for 2026aunimeda
Backend Engineering

PostgreSQL Performance Optimization: The Practical Guide for 2026

Slow queries, missing indexes, N+1 problems, and connection pool exhaustion account for 90% of PostgreSQL performance issues. Here's how to diagnose and fix each one with real queries.

Redis Data Structures in Production: Beyond SET and GETaunimeda
Backend Engineering

Redis Data Structures in Production: Beyond SET and GET

Most teams use Redis as a glorified hash map. This guide covers the data structures that solve real production problems - sorted sets for leaderboards, streams for durable event queues, HyperLogLog for UV counting at scale, and Lua scripts for atomic operations you can't otherwise do safely.

Need IT development for your business?

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

Get Consultation All articles