Skip to content

Web Server Performance Tuning: Complete Optimization Guide

DodaTech Updated 2026-06-23 8 min read

In this tutorial, you'll learn about Web Server Performance Tuning: Complete Optimization Guide. We cover key concepts, practical examples, and best practices.

Web server performance tuning optimizes the configuration of your web server to handle more concurrent connections, reduce latency, and improve throughput. A poorly tuned server can handle only a fraction of the traffic that a properly tuned server can, even on identical hardware.

In this tutorial, you will learn to tune NGINX and Apache worker processes, configure keepalive connections for maximum throughput, enable and optimize gzip compression, implement caching strategies, tune kernel parameters for web server workloads, and benchmark your server with tools like Apache Bench (ab) and wrk. DodaTech applies these tuning techniques to serve millions of requests per day across Doda Browser update infrastructure and Durga Antivirus Pro signature distribution servers.

What You'll Learn

By the end of this guide, you will tune NGINX and Apache to handle maximum concurrent connections, configure caching and compression for optimal throughput, tune Linux kernel parameters for web server workloads, and benchmark your server to measure improvements.

Why Performance Tuning Matters

Default web server configurations are conservative. They prioritize compatibility over performance. Tuning can double or triple your server's throughput without any hardware changes. This means lower latency for users, fewer servers needed, and reduced infrastructure costs. Every Web Servers administrator and Linux engineer should know how to tune servers for production workloads.

Performance Tuning Learning Path

flowchart LR
  A[Worker Processes] --> B[Keepalive and Connections]
  B --> C[Compression and Caching]
  C --> D[Kernel Tuning]
  D --> E[Benchmarking]
  E --> F{You Are Here}
  style F fill:#f90,color:#fff

NGINX Worker Process Tuning

NGINX uses an event-driven model. The number of worker processes should match CPU cores:

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_requests 1000;
    keepalive_timeout 65;

    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    output_buffers 32 32k;
    postpone_output 1460;

    open_file_cache max=2000 inactive=20s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors off;

    gzip on;
    gzip_comp_level 3;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
}

Calculating optimal worker_connections

# Formula: worker_connections = max_clients / worker_processes
# For 10000 max clients on a 4-core server:
# worker_connections = 10000 / 4 = 2500

# Check current system limits
ulimit -n

Expected output

65535

Apache MPM Tuning

Apache offers three Multi-Processing Modules. Choose the right one and tune it:

<IfModule mpm_event_module>
    StartServers             3
    MinSpareThreads          25
    MaxSpareThreads          75
    ThreadLimit              64
    ThreadsPerChild          25
    MaxRequestWorkers        400
    MaxConnectionsPerChild   10000
</IfModule>

MPM Prefork (For PHP compatibility, higher memory)

<IfModule mpm_prefork_module>
    StartServers             5
    MinSpareServers          5
    MaxSpareServers          10
    MaxRequestWorkers        150
    MaxConnectionsPerChild   1000
</IfModule>

Enable the optimal MPM

# Check current MPM
sudo apachectl -M | grep mpm

# Disable prefork, enable event
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo systemctl restart apache2

Expected output

mpm_event_module (static)

Compression and Caching

Gzip tuning (NGINX)

gzip on;
gzip_comp_level 3;
gzip_min_length 1000;
gzip_proxied any;
gzip_vary on;
gzip_types
    text/plain
    text/css
    application/json
    application/javascript
    text/xml
    application/xml
    image/svg+xml;

Cache control headers

# Static assets with versioning
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# HTML pages
location / {
    expires 1h;
    add_header Cache-Control "public, must-revalidate";
}

# API responses (no cache)
location /api/ {
    add_header Cache-Control "no-store, no-cache, must-revalidate";
    proxy_pass http://backend;
}

Proxy caching (NGINX)

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off;

server {
    location / {
        proxy_cache app_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
        proxy_cache_background_update on;
        proxy_cache_lock on;

        proxy_pass http://backend;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Expected cache header output

curl -I https://dodatech.com/assets/style.css
# cache-control: public, immutable
# x-cache-status: HIT

curl -I https://dodatech.com/api/products
# cache-control: no-store, no-cache, must-revalidate

Linux Kernel Tuning for Web Servers

# /etc/sysctl.d/99-web-server.conf

# Increase connection backlog
net.core.somaxconn = 65535

# Increase network buffer sizes
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728

# TCP buffer auto-tuning
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728

# Enable TCP Fast Open
net.ipv4.tcp_fastopen = 3

# Reuse TIME_WAIT sockets for new connections
net.ipv4.tcp_tw_reuse = 1

# Increase local port range
net.ipv4.ip_local_port_range = 1024 65535

# TCP keepalive
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 8

# Reduce SYN backlog threshold
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2

# Enable BBR congestion control
net.ipv4.tcp_congestion_control = bbr

# Increase file descriptor limit
fs.file-max = 2097152

Apply kernel settings:

sudo sysctl -p /etc/sysctl.d/99-web-server.conf

# Increase system-wide file descriptor limit
sudo bash -c 'echo "* soft nofile 65535" >> /etc/security/limits.conf'
sudo bash -c 'echo "* hard nofile 65535" >> /etc/security/limits.conf'

Benchmarking with Apache Bench (ab)

# Install Apache Bench
sudo apt install apache2-utils -y

# Basic benchmark: 1000 requests, 100 concurrent
ab -n 1000 -c 100 https://dodatech.com/

# With keepalive
ab -n 1000 -c 100 -k https://dodatech.com/

Expected benchmark output

Benchmarking dodatech.com (be patient)
Completed 1000 requests

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        5    8   2.3      7      24
Processing:    12   18   5.1     16      45
Waiting:        8   12   3.2     11      32
Total:         17   26   6.8     23      69

Requests per second:    3846.15 [#/sec] (mean)

Benchmarking with wrk (Modern Alternative)

# Install wrk
sudo apt install wrk -y

# Basic benchmark
wrk -t4 -c100 -d30s https://dodatech.com/

# With keepalive (default in HTTP/1.1)
wrk -t4 -c200 -d30s --latency https://dodatech.com/api/health

Expected output

Running 30s test @ https://dodatech.com
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    12.34ms    5.67ms  89.00ms   78.23%
    Req/Sec     2.15k   123.45     2.89k    72.50%
  Latency Distribution
     50%   11.00ms
     75%   15.00ms
     90%   20.00ms
     99%   35.00ms
  256000 requests in 30.00s, 1.24GB read
Requests/sec:   8533.33

Common Errors

1. Worker_connections Too Low

NGINX refuses connections when all workers are busy. Increase worker_connections or worker_processes. Monitor with curl -s http://localhost/nginx_status.

2. Apache Hitting MaxRequestWorkers

When all Apache workers are busy, new connections queue and eventually time out. Increase MaxRequestWorkers but watch for memory exhaustion (each prefork process uses 20-50 MB).

3. Gzip CPU Overhead Exceeds Benefit

Setting gzip_comp_level above 4 provides minimal extra compression with significant CPU cost. Level 3 is optimal for most content. For API responses, consider level 1.

4. Cache Not Hitting

The cache key does not match or the cache is too small. Check the proxy_cache_key format and increase keys_zone size. Use add_header X-Cache-Status to debug.

5. Kernel Buffer Size Too Large

Setting rmem_max and wmem_max too high can waste memory. Set values based on your workload's typical transfer sizes. 128 MB is safe for most web servers.

Practice Questions

1. What does worker_processes auto do in NGINX? It automatically sets the number of worker processes to match the number of CPU cores. This ensures optimal CPU utilization without oversubscribing.

2. Why is the event MPM preferred over prefork for Apache? The event MPM uses a thread-based model with an event loop, handling thousands of connections with fewer processes. Prefork creates a process per connection, consuming significantly more memory.

3. What is the purpose of the sendfile directive in NGINX? sendfile copies data directly from the file system to the network socket, bypassing user-space buffers. This reduces CPU usage and improves throughput for static file serving.

4. Challenge: Benchmark and optimize a web server

Run a benchmark against a web server before and after tuning:

  • Measure requests per second with ab (1000 requests, 100 concurrent)
  • Apply NGINX optimizations (worker_processes, sendfile, gzip, caching)
  • Tune kernel parameters (TCP Fast Open, BBR, buffer sizes)
  • Remeasure and calculate the improvement percentage
  • Document which change had the biggest impact

Mini Project: Maximum Throughput Tuning

Tune a web server for maximum throughput and benchmark the results:

  1. Configure NGINX with optimal worker_processes and worker_connections for a 4-core server
  2. Enable sendfile, tcp_nopush, and tcp_nodelay
  3. Configure gzip at level 3 for text-based content
  4. Set up proxy caching with a 1 GB cache zone
  5. Apply kernel tuning parameters for high-throughput web serving
  6. Benchmark with wrk (4 threads, 200 connections, 60 seconds)
  7. Benchmark with ab (10000 requests, 500 concurrent)
  8. Compare results with the default configuration
# Before tuning: capture baseline
ab -n 1000 -c 100 https://dodatech.com/ > baseline.txt

# After tuning: capture optimized
ab -n 1000 -c 100 https://dodatech.com/ > optimized.txt

# Calculate improvement
grep "Requests per second" baseline.txt
grep "Requests per second" optimized.txt

This tuning methodology is applied to every production server at DodaTech to ensure Doda Browser update distribution and Durga Antivirus Pro signature delivery meet their performance SLAs.

FAQ

How do I know if my web server is properly tuned?

Monitor CPU usage, memory, and connection queues. If CPU is below 70% under load, you can increase worker count. If connections queue up, increase worker_connections or MaxRequestWorkers. The goal is balanced resource utilization.

What is the single most impactful tuning change?

Enabling sendfile and configuring proper keepalive settings often provides the biggest improvement. sendfile reduces CPU for static files. Keepalive reduces TCP handshake overhead for clients making multiple requests.

Should I use HTTP/2 or HTTP/3 for better performance?

HTTP/2 reduces latency with multiplexing and header compression. HTTP/3 (QUIC) improves performance on lossy networks. Enable both if your web server supports them. HTTP/2 is essential; HTTP/3 is the next step.

How often should I re-benchmark my web server?

After every significant configuration change, OS update, or traffic pattern shift. Run a monthly benchmark as part of regular performance monitoring to detect regressions early.

What is the relationship between worker processes and memory?

Each worker process uses memory for connections, buffers, and caches. On a server with 4 GB RAM, limit total worker memory to 3 GB. Monitor with ps aux | grep nginx and check the RSS column.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro