Web Server Benchmarking with Apache Bench (ab)

Introduction

Apache Bench (ab) is a command-line tool for benchmarking HTTP web servers. Despite its name, it works with any web server (Apache, Nginx, IIS, etc.) and provides essential performance metrics including requests per second, response times, connection handling, and failure rates. Understanding how to properly use ab is crucial for performance testing, capacity planning, and optimization validation.

Proper benchmarking reveals how your web server performs under load, identifies bottlenecks, and validates the effectiveness of optimizations. Without accurate benchmarking, you're flying blind—making changes without knowing if they actually improve performance. Apache Bench provides a simple, reliable, and standardized way to measure web server performance.

This comprehensive guide covers Apache Bench fundamentals, usage techniques, result interpretation, advanced testing scenarios, and real-world examples. You'll learn how to conduct meaningful performance tests and make data-driven optimization decisions.

Installation

# Ubuntu/Debian
apt-get update
apt-get install apache2-utils -y

# CentOS/Rocky Linux
dnf install httpd-tools -y

# macOS (included with Apache)
# Already installed

# Verify installation
ab -V
# Output: This is ApacheBench, Version 2.3

Basic Usage

Simple Benchmark

# Basic syntax
ab [options] URL

# Simple test: 100 requests, 10 concurrent
ab -n 100 -c 10 http://localhost/

# Options explained:
# -n 100: Total requests to perform
# -c 10: Number of concurrent connections

Understanding Output

ab -n 1000 -c 100 http://localhost/test.html

# Output example:
Server Software:        nginx/1.24.0
Server Hostname:        localhost
Server Port:            80

Document Path:          /test.html
Document Length:        1234 bytes

Concurrency Level:      100
Time taken for tests:   2.456 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      1456789 bytes
HTML transferred:       1234000 bytes
Requests per second:    407.19 [#/sec] (mean)
Time per request:       245.596 [ms] (mean)
Time per request:       2.456 [ms] (mean, across all concurrent requests)
Transfer rate:          579.42 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.5      1       3
Processing:    12  235  45.2    228     456
Waiting:       11  234  45.1    227     455
Total:         13  236  45.3    229     457

Percentage of the requests served within a certain time (ms)
  50%    229
  66%    245
  75%    258
  80%    267
  90%    295
  95%    318
  98%    354
  99%    387
 100%    457 (longest request)

Key Metrics Explained

Requests Per Second

# Most important metric
Requests per second:    407.19 [#/sec] (mean)

# Interpretation:
# - Higher is better
# - Indicates server throughput capacity
# - Baseline for comparison

# Example comparison:
# Before optimization: 407 req/s
# After optimization: 1,842 req/s (4.5x improvement)

Response Time

# Two important metrics:
Time per request:       245.596 [ms] (mean)  # User perspective
Time per request:       2.456 [ms] (mean, across all concurrent requests)  # Server perspective

# User perspective:
# - Time from request start to completion
# - What users actually experience
# - Includes waiting for concurrent requests

# Server perspective:
# - Actual processing time per request
# - Server efficiency metric
# - Excludes concurrency waiting

Percentiles

Percentage of the requests served within a certain time (ms)
  50%    229  # Median - typical request
  90%    295  # 90% of users see this or better
  95%    318  # 95th percentile
  99%    387  # 99th percentile
 100%    457  # Worst case (max)

# Why percentiles matter:
# - Average can hide outliers
# - 95th/99th percentile = user experience for slowest users
# - p50, p95, p99 are industry standard metrics

Failed Requests

Complete requests:      1000
Failed requests:        0  # Should always be 0 in healthy system

# Types of failures:
# - Connection refused (server overloaded)
# - Timeout (request took too long)
# - Length mismatch (inconsistent responses)
# - Exceptions (server errors)

# If > 0: Indicates problems, investigate before optimizing

Testing Scenarios

Baseline Performance Test

# Establish baseline with low concurrency
ab -n 1000 -c 10 http://localhost/

# Results:
# Requests per second: 1,250
# Response time: 8ms (mean)
# Failed requests: 0

# Purpose: Measure best-case performance

Stress Test

# High concurrency to test limits
ab -n 10000 -c 1000 http://localhost/

# Results:
# Requests per second: 2,840
# Response time: 352ms (mean)
# Failed requests: 0

# Purpose: Find maximum capacity

Capacity Planning

# Test at different concurrency levels
for concurrency in 10 50 100 200 500 1000; do
    echo "Testing concurrency: $concurrency"
    ab -n 5000 -c $concurrency http://localhost/ 2>&1 | grep "Requests per second"
done

# Results show scaling behavior:
# c=10:   1,250 req/s
# c=50:   2,450 req/s
# c=100:  3,180 req/s
# c=200:  3,520 req/s (plateau starting)
# c=500:  3,610 req/s (minimal gain)
# c=1000: 3,450 req/s (performance degradation)

# Conclusion: Optimal performance at c=200-500

Sustained Load Test

# Long-duration test to check stability
ab -n 100000 -c 100 -t 300 http://localhost/

# -t 300: Run for 300 seconds (5 minutes)
# Purpose: Detect memory leaks, connection exhaustion, etc.

# Monitor during test:
watch -n 1 'ps aux | grep nginx; free -h; ss -s'

Advanced Options

Keep-Alive Testing

# Without keep-alive (new connection each request)
ab -n 1000 -c 100 http://localhost/

# With keep-alive (reuse connections)
ab -n 1000 -c 100 -k http://localhost/

# Comparison:
# Without keep-alive: 2,350 req/s, 42ms response
# With keep-alive: 4,180 req/s, 24ms response (78% faster)

# Lesson: Keep-alive dramatically improves performance

POST Requests

# Test POST with data
ab -n 1000 -c 100 -p post-data.txt -T "application/json" http://localhost/api/users

# Create post-data.txt:
cat > post-data.txt << 'EOF'
{"name":"John Doe","email":"[email protected]"}
EOF

# Options:
# -p: POST data file
# -T: Content-Type header

Custom Headers

# Add authentication header
ab -n 1000 -c 100 -H "Authorization: Bearer token123" http://localhost/api/protected

# Add multiple headers
ab -n 1000 -c 100 \
   -H "Authorization: Bearer token123" \
   -H "Accept: application/json" \
   -H "User-Agent: LoadTest/1.0" \
   http://localhost/api/

# Cookie header
ab -n 1000 -c 100 -C "session_id=abc123" http://localhost/

Timeout Configuration

# Set socket timeout (default: 30 seconds)
ab -n 1000 -c 100 -s 60 http://localhost/slow-endpoint

# -s 60: 60 second timeout
# Useful for testing slow endpoints

Output to File

# Save results to file
ab -n 1000 -c 100 -g results.tsv http://localhost/

# TSV format for analysis:
# starttime  seconds  ctime  dtime  ttime  wait
# Each row = one request timing

# Generate CSV with key metrics
ab -n 1000 -c 100 http://localhost/ > results.txt
grep "Requests per second\|Time per request\|Failed requests" results.txt > summary.csv

Real-World Testing Examples

Example 1: Before/After Optimization

#!/bin/bash
# compare-performance.sh

URL="http://localhost/"
REQUESTS=5000
CONCURRENCY=500

echo "=== BEFORE Optimization ==="
ab -n $REQUESTS -c $CONCURRENCY $URL | grep -E "Requests per second|Time per request|Failed requests|Transfer rate"

# Make optimizations (enable caching, compression, etc.)
echo
echo "Making optimizations..."
# ... apply optimizations ...
sleep 5

echo
echo "=== AFTER Optimization ==="
ab -n $REQUESTS -c $CONCURRENCY $URL | grep -E "Requests per second|Time per request|Failed requests|Transfer rate"

# Results:
# BEFORE:
# Requests per second: 1,450
# Time per request: 344ms
# Failed requests: 67

# AFTER:
# Requests per second: 5,280 (264% improvement)
# Time per request: 95ms (72% faster)
# Failed requests: 0 (100% reliable)

Example 2: Static vs Dynamic Content

# Test static content (HTML file)
echo "Static content:"
ab -n 10000 -c 500 -k http://localhost/static.html | grep "Requests per second"
# Result: 8,450 req/s

# Test dynamic content (PHP script)
echo "Dynamic content:"
ab -n 10000 -c 500 -k http://localhost/dynamic.php | grep "Requests per second"
# Result: 1,240 req/s

# Conclusion: Static 6.8x faster (expected)
# Action: Cache dynamic content or use CDN

Example 3: Database Query Performance

# Test endpoint with database queries
ab -n 1000 -c 100 http://localhost/api/products

# Before database optimization:
# Requests per second: 85
# Time per request: 1,176ms
# 95th percentile: 2,340ms

# After adding indexes and query optimization:
# Requests per second: 620 (629% improvement)
# Time per request: 161ms (86% faster)
# 95th percentile: 287ms (88% faster)

Example 4: API Rate Limiting Test

# Test API rate limiting
ab -n 10000 -c 100 http://localhost/api/data

# Check for 429 (Too Many Requests) responses
ab -n 10000 -c 100 http://localhost/api/data | grep "Non-2xx responses"
# Non-2xx responses: 3,450 (rate limit working)

# Adjust concurrency to stay under limit
ab -n 10000 -c 10 http://localhost/api/data | grep "Failed requests"
# Failed requests: 0 (within rate limit)

Example 5: CDN vs Origin Performance

# Test origin server
echo "Origin server:"
ab -n 1000 -c 100 http://origin.example.com/large-image.jpg
# Requests per second: 450
# Transfer rate: 12,560 KB/sec

# Test CDN
echo "CDN:"
ab -n 1000 -c 100 http://cdn.example.com/large-image.jpg
# Requests per second: 3,280 (629% improvement)
# Transfer rate: 91,540 KB/sec (629% faster)

# Conclusion: CDN provides 7x better performance

Benchmarking Best Practices

1. Test from Appropriate Location

# Bad: Testing from same server (unrealistic)
ab -n 1000 -c 100 http://localhost/

# Good: Test from separate machine
ab -n 1000 -c 100 http://server-ip/

# Better: Test from geographically distant location
ab -n 1000 -c 100 http://remote-server.com/

# Best: Test from multiple locations and average results

2. Warm Up Before Testing

# Warm-up requests (prime caches)
ab -n 100 -c 10 http://localhost/

# Then actual test
ab -n 10000 -c 500 http://localhost/

# Prevents cold-start effects from skewing results

3. Test Multiple Times

#!/bin/bash
# Run multiple tests and average results

RUNS=5
URL="http://localhost/"

echo "Running $RUNS benchmark tests..."

for i in $(seq 1 $RUNS); do
    echo "Test $i of $RUNS"
    ab -n 1000 -c 100 $URL 2>&1 | grep "Requests per second" >> results.txt
done

# Calculate average
awk '{sum += $4; count++} END {print "Average:", sum/count, "req/s"}' results.txt

# Output: Average: 4,235 req/s

4. Monitor System Resources

# Monitor during test (separate terminal)
watch -n 1 'echo "CPU:"; mpstat 1 1; echo "Memory:"; free -h; echo "Connections:"; ss -s'

# Or use tool like htop, glances, or nmon
htop

# Check for bottlenecks:
# - CPU at 100%: Need more CPU or optimize code
# - Memory exhausted: Add RAM or optimize memory usage
# - High I/O wait: Disk bottleneck, use SSD or optimize queries

5. Test Realistic Scenarios

# Bad: Testing only homepage
ab -n 10000 -c 100 http://localhost/

# Good: Test mix of endpoints
ab -n 2000 -c 100 http://localhost/
ab -n 2000 -c 100 http://localhost/products
ab -n 2000 -c 100 http://localhost/about
ab -n 2000 -c 100 http://localhost/contact

# Better: Use tool that supports URL lists (e.g., wrk, siege)

Interpreting Results

Identifying Bottlenecks

# Scenario 1: High requests/sec but high failure rate
# Complete requests: 8,000
# Failed requests: 2,000 (20% failure)
# Diagnosis: Server overloaded, max_connections or worker processes too low
# Solution: Increase server resources or connection limits

# Scenario 2: Low requests/sec, low CPU
# Requests per second: 150
# CPU usage: 25%
# Diagnosis: Blocking I/O, database queries, or external API calls
# Solution: Add caching, optimize queries, implement async processing

# Scenario 3: Good performance drops over time
# Start: 3,500 req/s
# After 5 min: 1,200 req/s
# Diagnosis: Memory leak, connection exhaustion, log file growth
# Solution: Fix memory leaks, implement connection pooling, rotate logs

# Scenario 4: High variation in response times
# Mean: 150ms
# 99th percentile: 4,500ms
# Diagnosis: Outliers, garbage collection pauses, or occasional slow queries
# Solution: Investigate slow requests, tune GC, add query timeouts

Performance Goals

# Typical targets by application type:

# Static website:
# > 5,000 req/s (good), > 10,000 req/s (excellent)

# Dynamic website (WordPress, etc.):
# > 100 req/s (good), > 500 req/s (excellent)

# API server:
# > 1,000 req/s (good), > 5,000 req/s (excellent)

# Database-heavy application:
# > 50 req/s (good), > 200 req/s (excellent)

# Real-time application:
# > 10,000 req/s (good), > 50,000 req/s (excellent)

# Response time targets:
# < 100ms: Excellent
# < 300ms: Good
# < 1s: Acceptable
# > 1s: Needs improvement

Limitations and Alternatives

Apache Bench Limitations

  1. Single URL: Can't test multiple URLs in one test
  2. No JavaScript: Can't execute JavaScript (only fetches HTML)
  3. Simple scenarios: No complex user flows
  4. Single server: All requests from one machine

Alternative Tools

# wrk - Modern, scriptable benchmarking tool
wrk -t 12 -c 400 -d 30s http://localhost/

# siege - Supports multiple URLs
siege -c 100 -r 10 -f urls.txt

# hey - Go-based, similar to ab
hey -n 10000 -c 100 http://localhost/

# locust - Python-based, complex scenarios
# Requires script, good for realistic user behavior

# JMeter - GUI-based, enterprise features
# Good for complex test plans and reporting

Complete Testing Script

#!/bin/bash
# comprehensive-benchmark.sh

URL="$1"
OUTPUT_DIR="benchmark-results-$(date +%Y%m%d-%H%M%S)"

mkdir -p "$OUTPUT_DIR"

echo "Starting comprehensive benchmark of $URL"
echo "Results will be saved to $OUTPUT_DIR"
echo

# Test 1: Baseline
echo "Test 1: Baseline (low concurrency)"
ab -n 1000 -c 10 -k "$URL" > "$OUTPUT_DIR/01-baseline.txt"
grep "Requests per second\|Time per request" "$OUTPUT_DIR/01-baseline.txt"
echo

# Test 2: Moderate load
echo "Test 2: Moderate load"
ab -n 5000 -c 100 -k "$URL" > "$OUTPUT_DIR/02-moderate.txt"
grep "Requests per second\|Time per request\|Failed requests" "$OUTPUT_DIR/02-moderate.txt"
echo

# Test 3: High load
echo "Test 3: High load"
ab -n 10000 -c 500 -k "$URL" > "$OUTPUT_DIR/03-high-load.txt"
grep "Requests per second\|Time per request\|Failed requests" "$OUTPUT_DIR/03-high-load.txt"
echo

# Test 4: Stress test
echo "Test 4: Stress test (find limits)"
ab -n 20000 -c 1000 -k "$URL" > "$OUTPUT_DIR/04-stress.txt"
grep "Requests per second\|Time per request\|Failed requests" "$OUTPUT_DIR/04-stress.txt"
echo

# Test 5: Sustained load
echo "Test 5: Sustained load (60 seconds)"
ab -t 60 -c 100 -k "$URL" > "$OUTPUT_DIR/05-sustained.txt"
grep "Requests per second\|Time per request" "$OUTPUT_DIR/05-sustained.txt"
echo

# Generate summary
echo "=== Benchmark Summary ===" > "$OUTPUT_DIR/summary.txt"
echo >> "$OUTPUT_DIR/summary.txt"
for file in "$OUTPUT_DIR"/*.txt; do
    echo "$(basename $file):" >> "$OUTPUT_DIR/summary.txt"
    grep "Requests per second" "$file" >> "$OUTPUT_DIR/summary.txt"
    echo >> "$OUTPUT_DIR/summary.txt"
done

echo "Benchmarking complete. Results in $OUTPUT_DIR/"
cat "$OUTPUT_DIR/summary.txt"
# Usage
chmod +x comprehensive-benchmark.sh
./comprehensive-benchmark.sh http://localhost/

Conclusion

Apache Bench is an essential tool for web server performance testing. Key takeaways:

When to Use ab:

  • Quick performance tests
  • Before/after optimization comparison
  • Capacity planning
  • Regression testing
  • CI/CD performance validation

Key Metrics:

  • Requests per second (throughput)
  • Response time (latency)
  • Percentiles (95th, 99th)
  • Failed requests (reliability)

Best Practices:

  • Test from separate machine
  • Warm up before testing
  • Run multiple tests and average
  • Monitor system resources
  • Test realistic scenarios
  • Use keep-alive (-k flag)

Performance Targets:

  • Static content: > 5,000 req/s
  • Dynamic content: > 100-500 req/s
  • APIs: > 1,000 req/s
  • Response time: < 300ms (good), < 100ms (excellent)

Apache Bench provides quick, reliable performance metrics that enable data-driven optimization decisions. Use it regularly to validate changes and ensure your web server performs optimally under load.