Optimizing High TTFB in WordPress: Debugging Nginx + PHP-FPM + Varnish Stack on Linode VPS


2 views

When dealing with WordPress performance, nothing frustrates more than watching that yellow "Wait" bar in Pingdom tools. Here's what I discovered when my Linode VPS showed 2.8s TTFB but sub-200ms page generation after that initial delay.

# Nginx 1.0.5 config snippet (truncated)
server {
    listen 80;
    server_name example.com;
    root /var/www/example;
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Initially suspected Varnish as the culprit, but headers showed cache misses weren't the issue. The key was in the backend handoff:

# Varnish 3.0 VCL snippet
backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 600s;
    .first_byte_timeout = 600s;
    .between_bytes_timeout = 600s;
}

The game-changer was adjusting PHP-FPM's process manager settings. Default values choked under load:

; /etc/php5/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
request_terminate_timeout = 30s

Enabled slow query logging and found unoptimized WordPress queries:

# my.cnf additions
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 2
log_queries_not_using_indexes = 1
  • Added Redis object caching for WordPress
  • Configured Nginx fastcgi buffers properly
  • Optimized WordPress database tables
  • Implemented OPcache (replacing APC)

Result? TTFB dropped from 2800ms to 89ms under similar load conditions.


When analyzing web performance metrics, Time To First Byte (TTFB) often reveals backend bottlenecks before other symptoms appear. In this case, we're observing:

  • 5-8 second TTFB on a production WordPress site
  • Normal server load during regular traffic (1-2 on htop)
  • Server overload during benchmarking (15+ load average)
  • Dramatic difference between a test site (fast TTFB) and production site (slow TTFB)

From analyzing the provided configurations, these areas need immediate attention:

# PHP-FPM critical settings (www.conf)
pm = dynamic
pm.max_children = 50  # May need adjustment based on RAM
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
request_terminate_timeout = 30s

The MySQL configuration shows several opportunities for optimization:

# Key my.cnf adjustments
innodb_buffer_pool_size = 256M  # For 1GB VPS
innodb_log_file_size = 64M
query_cache_size = 32M
table_open_cache = 2000
tmp_table_size = 32M
max_heap_table_size = 32M

The current nginx configuration can be enhanced with these WordPress-specific tweaks:

# In nginx server block
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;

# For Varnish backend
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;

For high-traffic WordPress sites, implement these in wp-config.php:

define('WP_CACHE', true);
define('WP_MEMORY_LIMIT', '128M');
define('WP_MAX_MEMORY_LIMIT', '256M');
define('EMPTY_TRASH_DAYS', 3);
define('WP_POST_REVISIONS', 5);

Essential plugins for performance:

  • Query Monitor (debugging)
  • Redis Object Cache
  • WP Super Cache (with late init)
  • Autoptimize (CSS/JS optimization)

To properly benchmark without crashing the server:

ab -n 100 -c 10 -k -H "Accept-Encoding: gzip,deflate" http://example.com/
siege -b -t1M -c25 http://example.com/

Key monitoring commands during tests:

# Real-time monitoring
htop
iotop -o
mytop
netstat -tulnp | grep :80

For Varnish 3.0 with WordPress, this VCL snippet handles cache purging:

backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 600s;
    .first_byte_timeout = 600s;
    .between_bytes_timeout = 600s;
}

sub vcl_recv {
    if (req.request == "PURGE") {
        if (!client.ip ~ purge) {
            error 405 "Not allowed.";
        }
        return(lookup);
    }
    if (req.url ~ "^/wp-(login|admin|cron)") {
        return(pass);
    }
}

Remember to configure WordPress to send proper cache headers:

# In theme functions.php
add_action('send_headers', 'add_cache_headers');
function add_cache_headers() {
    if (!is_user_logged_in() && !is_admin()) {
        header('Cache-Control: public, max-age=3600');
    }
}