Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How Memory Management Works in Web Servers and Applications

Published

System ManagementSystem Administration

Memory management is one of those topics that sounds boring until your server crashes at 3 AM with an out-of-memory error. Then it becomes the most interesting topic in the world. Web servers and applications handle memory in ways that look simple from the outside but involve a lot of moving parts underneath, and understanding those parts is the difference between a server that runs for months and one that needs a weekly reboot.

In this article, you will learn how operating systems allocate memory to processes, how web servers like Nginx and Apache differ in their memory models, how application runtimes like Node.js and PHP manage memory differently, and how to spot and fix memory problems before they take down production.

Prerequisites

Before you start, make sure you have:

  • A Linux machine running Ubuntu 22.04, Debian 12, or a similar distribution.
  • A user account with sudo access.
  • Basic familiarity with the command line.
  • A web server like Nginx or Apache installed for hands-on testing.

How the Operating System Hands Out Memory

Before talking about web servers, it helps to understand what is happening one layer below them. When a process starts, the kernel gives it a chunk of virtual memory. This is not actual Random Access Memory (RAM). It is a promise that says “you can use this address space, and we will figure out the physical memory later.”

The process asks for memory through system calls like malloc in C, which eventually become brk or mmap calls to the kernel. The kernel maps virtual addresses to physical RAM only when the process actually touches the memory. This is called demand paging, and it is why a process can show enormous virtual memory usage while using almost no real RAM.

Memory is divided into pages, usually 4 KB each. The kernel tracks which pages belong to which process and which pages are currently in physical memory versus swap. When physical memory runs low, the kernel reclaims pages from various sources, sometimes from inactive processes, sometimes from the file cache, sometimes by swapping anonymous pages to disk.

This matters for web servers because every connection, every request, and every cached object lives in memory somewhere. The way a server organizes its workers and threads decides how that memory gets used.

How Web Servers Use Memory

Different web servers take different approaches to handling memory, and the choice has real consequences for how the server scales.

Apache and the Process-Per-Request Model

Apache traditionally uses a model where each connection or request gets its own process or thread. The exact behavior depends on the Multi-Processing Module (MPM) you have configured. The most common ones are prefork, worker, and event.

With prefork, Apache spawns multiple child processes, and each one handles a single connection at a time. Each process loads its own copy of Apache, plus any modules, plus the application code if you are using something like mod_php. A single Apache prefork worker with PHP loaded might use 30 to 50 MB of RAM. Multiply that by 100 workers and you are looking at 3 to 5 GB of memory just for the web server.

The worker and event MPMs use threads inside processes, which is much more memory efficient. A handful of processes with many threads each can serve thousands of connections without eating gigabytes of RAM.

Check which Apache MPM is currently active.

apache2ctl -V | grep -i mpm

Look at the actual memory usage of Apache workers.

ps -ylC apache2 --sort:rss

The Resident Set Size (RSS) column shows real memory usage in kilobytes for each process.

Nginx and the Event-Driven Model

Nginx took a fundamentally different approach. Instead of one process per connection, Nginx uses a small number of worker processes, usually one per CPU core, and each worker handles thousands of connections at the same time using non-blocking input and output.

The result is dramatically lower memory usage. An Nginx worker handling 10,000 concurrent connections typically uses less RAM than a single Apache prefork worker handling one connection. This is why Nginx became popular for high-traffic sites. The memory math just works out better.

Nginx still needs memory for connection state, request buffers, and shared caches, but it does not duplicate the entire server stack for every connection.

Check Nginx worker count and memory usage.

ps -ylC nginx --sort:rss

See connection counts per worker.

sudo nginx -T | grep worker

How Application Runtimes Handle Memory

The web server is only half the story. The application running behind it has its own memory model, and the two interact in ways that can either work smoothly or burn through RAM at frightening speed.

PHP and Per-Request Memory

PHP traditionally treats each request as an isolated event. The script runs, allocates whatever memory it needs, sends the response, and then everything gets thrown away. PHP-FPM workers handle requests one at a time, and the memory footprint of a worker depends on what scripts it has loaded.

The setting that matters most here is memory_limit in php.ini. This limits how much memory a single PHP script can use before it errors out. The default is 128 MB, which is usually fine for normal applications but can be too low for image processing or large data exports.

Check the current PHP memory limit.

php -r "echo ini_get('memory_limit');"

Find the active php.ini file.

php --ini

PHP-FPM also has its own settings for how many workers to keep around. The pm.max_children setting controls the maximum number of worker processes. Each worker uses memory proportional to the largest script it has run, so a few rogue scripts can balloon your memory usage even after the requests finish.

[!NOTE] PHP-FPM workers do not always release memory back to the operating system after a request finishes. The memory stays reserved for future requests, which is fast but can look alarming in monitoring graphs.

Node.js and Long-Lived Processes

Node.js works completely differently. A Node.js application runs as a single long-lived process that handles many requests over time. Memory accumulates across requests, gets garbage collected when possible, and grows or shrinks based on how the application is written.

The default heap size in Node.js is around 1.5 GB on 64-bit systems. Applications that need more have to start the process with a larger limit.

node --max-old-space-size=4096 app.js

The biggest memory issue in Node.js is the memory leak. Because the process keeps running, any object that gets accidentally retained will pile up over time. Common culprits are global variables, event listeners that are never removed, and cached data that grows without bound.

Python and the Worker Pool Model

Python web applications usually run behind a Web Server Gateway Interface (WSGI) server like Gunicorn or uWSGI. These servers run a pool of worker processes, similar to PHP-FPM but with more flexibility.

Each Python worker loads the full application into memory at startup. A Django or Flask application with all its dependencies might use 100 to 300 MB per worker before serving any requests. With 8 workers running, that is 800 MB to 2.4 GB just for the application.

Python also has its own quirks around memory. The interpreter does not always return freed memory to the operating system, and certain patterns like loading large datasets into Pandas DataFrames can cause memory usage to spike and stay high.

Memory Caching at the Application Layer

Web servers and applications both use caching aggressively to avoid recomputing things or hitting the database for every request. Caching trades memory for speed, and the tradeoff is usually worth it, but only if you understand what is being cached.

OPcache for PHP

PHP has to parse and compile every script for every request unless you enable OPcache. OPcache keeps the compiled bytecode in shared memory, so scripts run much faster after the first request.

Check if OPcache is enabled.

php -i | grep -i opcache

The relevant setting is opcache.memory_consumption in php.ini, usually set to 128 MB or 256 MB. Too small a cache means scripts get evicted and recompiled often. Too large wastes RAM.

In-Memory Caches for Applications

Applications often add their own caching layer using tools like Redis or Memcached. These run as separate processes that store data in memory, and the application talks to them over the network or a Unix socket.

Redis and Memcached are extremely memory efficient compared to caching inside the application process. They also let multiple application workers share the same cache, which prevents each worker from holding its own copy of the same data.

The catch is that uncontrolled cache growth is a common problem. A cache without an eviction policy will keep growing until it consumes all available memory and then either crashes or starts swapping to disk, which kills performance.

[!WARNING] Always set a maximum memory limit and an eviction policy for Redis. Without these, the cache will keep filling until the operating system kills the process or the entire server slows to a crawl.

How to Spot Memory Problems

Memory issues usually show up in one of three ways. The server runs out of RAM and starts swapping heavily. A specific process grows over time and eventually gets killed by the kernel’s Out-Of-Memory (OOM) killer. Or the application slows down because garbage collection is running constantly.

Check Overall Memory Usage

Get a quick overview of memory state.

free -h

Look at the available column. This is the more honest number than free, because it accounts for cache that can be reclaimed quickly.

Watch memory usage over time.

vmstat 5

The command updates every 5 seconds. Keep an eye on the si and so columns, which show swap-in and swap-out activity. Consistent swapping means the server does not have enough RAM for its workload.

Find Memory-Hungry Processes

List the top memory consumers.

ps aux --sort=-%mem | head

The output shows the processes using the most memory, sorted from highest to lowest. The %MEM column shows the percentage of system memory each process holds.

Get a live view with sortable columns.

top -o %MEM

Check for OOM Kills

When the kernel runs out of memory, it picks a process to kill. The decision usually targets whatever was using the most memory at the time, which can be your application server or database.

Look for OOM kills in the system log.

sudo dmesg | grep -i "out of memory"

Check the journal for OOM events.

sudo journalctl -k | grep -i oom

If you see OOM messages, the server is undersized for its workload, or something is leaking memory. Both need to be addressed before they happen again at a less convenient time.

Practical Tuning for Memory Efficiency

Once you know how memory flows through the system, the tuning becomes a matter of matching configuration to actual usage.

  • For Apache, switch from prefork to event MPM if your application allows it. This alone can cut memory usage by 60 to 80 percent on a busy server. If you must stay on prefork, reduce MaxRequestWorkers to a number your RAM can actually support.
  • For Nginx, the defaults are usually fine. The most common mistake is setting worker_connections too high without enough RAM to back it up. A reasonable starting point is 1024 connections per worker, scaled up only after monitoring real usage.
  • For PHP-FPM, set pm.max_children based on memory math. Take your available RAM, subtract what other services need, and divide by the average memory per worker. If a PHP-FPM worker uses 60 MB and you have 4 GB available, you can support roughly 60 to 65 workers.
  • For Node.js, set --max-old-space-size to a value that fits the server’s RAM, with headroom for the operating system and other processes. Monitor heap usage with built-in tools or Application Performance Monitoring (APM) services.
  • For Redis and Memcached, always set maxmemory and an eviction policy like allkeys-lru. This prevents the cache from consuming all available RAM during traffic spikes.

Conclusion

You now understand how the operating system gives memory to processes, how Apache and Nginx differ in their memory models, how PHP, Node.js, and Python runtimes manage memory differently, and how application-layer caching fits into the picture. You also know how to find memory-hungry processes, detect OOM kills, and tune common services for better memory efficiency.

Memory management is not glamorous work, but it pays off. A server that uses memory efficiently runs faster, costs less, and stays up longer. The investment in understanding these patterns is small compared to the cost of an unexpected outage. Watch the metrics, tune the obvious settings, and keep an eye on growth over time. The rest tends to take care of itself.

Related guides

Account services are unavailable until the production backend is configured.