The most common misconception about slow Time to First Byte (TTFB) is that it is a hardware problem. Developers routinely blame their hosting provider, assume they need a bigger instance, or jump straight to a CDN. Then they pay for the upgrade, watch the metrics, and find the response time has barely moved.

In practice, a slow server response is usually a configuration problem. The request is hitting the right server, but the software stack is wasting hundreds of milliseconds before sending a single byte back. This guide walks through the five mistakes that cause this waste, in the order you should investigate them, with concrete commands to verify each fix.


Step 1: Measure TTFB Correctly (Before You Change Anything)

You cannot fix what you cannot isolate. A raw curl command gives you a single number, but it does not tell you where the time is going.

curl -o /dev/null -s -w "Connect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://yourdomain.com/

Run that command several times. Look at the gap between Connect and TTFB. The connection time is largely outside your control (DNS, TLS handshake, network latency). The gap between connect and TTFB is your server’s processing time. That is your target.

If the gap is small (under 100ms) on a local test but the site feels slow, the bottleneck is elsewhere: the browser, the frontend, or a third-party script. If the gap is consistently over 300ms, continue to Step 2.

One measurement is a data point. Ten measurements over a few minutes give you a baseline. Save the output to a file for comparison later.


Step 2: Check for an Overloaded or Misconfigured Reverse Proxy

Nginx and Apache are often the culprit, not because they are slow, but because they are configured to wait for something that never comes. The most common misconfiguration involves proxy timeouts that are set too high, causing the web server to hold the connection open while the upstream application hangs.

Start by looking at the active connections and the upstream response times.

# Check if nginx is overloaded with connections
ss -s
# Look at recent error logs for upstream timeouts
sudo tail -100 /var/log/nginx/error.log

A common fix is to reduce the proxy timeout values from the default 60 seconds to something that fails fast. You want the request to error out quickly so you can see the problem, not silently wait.

# /etc/nginx/conf.d/timeouts.conf
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;

Apply the change and reload.

sudo nginx -t && sudo systemctl reload nginx

The error log will now tell you if the application is the problem. If you see upstream timed out messages, the issue is in your application code or the connection between Nginx and your app server, not in the reverse proxy itself.


Step 3: Inspect Your Application’s Slow Query Log

If the reverse proxy is passing requests cleanly but the response is still slow, the bottleneck is inside the application. The most frequent cause is a database query that works in development but falls apart under production data volumes.

Enable the slow query log on MySQL or PostgreSQL to see what is running long.

-- MySQL: Enable slow query log (adjust path for your system)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- logs queries taking longer than 1 second
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-queries.log';

Then reproduce the slow request. Open the page that is taking long, and then inspect the log.

sudo tail -50 /var/log/mysql/slow-queries.log

You will often find a query missing an index or performing a full table scan. The fix is rarely to rewrite the query; it is usually to add the correct index.

EXPLAIN SELECT * FROM orders 
WHERE user_id = 123 AND status = 'pending' 
ORDER BY created_at DESC;

If the EXPLAIN output shows type: ALL or a high rows value, add a composite index matching the WHERE clause.

CREATE INDEX idx_user_status_created 
ON orders (user_id, status, created_at);

Run the EXPLAIN again. The row count should drop dramatically. Re-run the curl test from Step 1 and compare the TTFB.


Step 4: Verify PHP-FPM Pool Settings (If You Use PHP)

PHP-FPM is a frequent source of hidden latency. The default pool settings are conservative. When the pool is exhausted, new requests queue up waiting for a free worker process. This queue time appears directly in your TTFB measurement.

Check the current pool status.

# Add this to your PHP-FPM pool config (usually /etc/php/8.x/fpm/pool.d/www.conf)
pm.status_path = /status

Reload PHP-FPM and then query the status endpoint.

sudo systemctl reload php8.1-fpm
curl http://127.0.0.1/status?full

Look at the listen queue value. If it is consistently above zero, your pool is undersized. The fix is to increase the number of workers, but not blindly.

Inspect the memory usage per worker first.

ps aux | grep php-fpm

Check the %MEM column. If each worker is using 200MB and your server has 4GB of RAM, you can afford roughly 15-18 workers. Do not set pm.max_children to 50 if your RAM cannot support it. The server will swap, and TTFB will get worse.

A safer adjustment is to change the process manager from dynamic to on-demand if your traffic is bursty.

pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s

This keeps only a few workers alive during quiet periods and spawns new ones only when traffic arrives.


Step 5: Replace Repeated Filesystem Operations with Cache

A slow server response is often a server that is doing the same work on every request. Session file writes, template compilation, and config parsing happen repeatedly. The disk becomes the bottleneck.

The fastest fix is often to move session storage out of files and into memory. Redis is the standard solution.

First, verify Redis is running and accepting connections.

redis-cli ping
# Should return PONG

Then point your application to use it. For PHP sessions, the change is in the php.ini or the pool config.

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"

For a Node.js or Python application, the same principle applies: replace synchronous filesystem reads with an in-memory cache for data that does not change often.

A simpler win is to enable opcode caching for PHP. This stores compiled PHP scripts in memory, avoiding recompilation on every request. Verify it is enabled.

php -i | grep opcache.enable

If it returns opcache.enable => Off, enable it in your php.ini.

opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000

After making these changes, run the curl test from Step 1 one final time. The gap between Connect and TTFB should be measurably smaller.


When Not to Apply These Fixes

Each of these fixes has a failure mode. Do not increase PHP-FPM workers if you have not verified you have the RAM. Do not add a Redis server if your application only gets fifty requests per day; the added infrastructure will not justify itself. And do not disable session file storage if you have a multi-server setup without a shared session store.

The step-by-step approach exists to prevent random config changes. If Step 3 shows no slow queries, do not add indexes. If Step 2 shows no upstream timeouts, do not change the proxy config. Each step gives you a piece of evidence that tells you whether to move forward or stop.

What does your time_starttransfer look like compared to time_connect? The gap between those two numbers tells you exactly where to start digging.