Skip to main content

Cut TTFB on a Linux VPS with Nginx Cache, TCP BBR, and Redis

TTFB sets the floor for how fast your site feels. Here is how to tune Nginx caching, TCP BBR, and Redis on a Linux VPS, step by step.

Cover image for Cut TTFB on a Linux VPS with Nginx Cache, TCP BBR, and Redis

Time to First Byte (TTFB) is the hard floor for site speed. If your host waits 750 ms, the browser waits too. That delay comes first. It lands before the first byte of HTML. No work in the browser can fix it. The Largest Contentful Paint (LCP) score cannot beat that wait.

This post is for site owners and admins. A slow TTFB costs you money and traffic. Google reads real user data. The source is the Chrome User Experience Report (CrUX). That window rolls over 28 days. A slow TTFB pushes your page out of the good band. That can push your site down in search. AI search tools such as ChatGPT Search and Perplexity run on tight timers. A bot that waits too long quits. It cites a faster site instead.

You want 100 ms TTFB or less. On a Linux virtual private server (VPS), that means fixing three layers. First the Linux stack. Then the Nginx proxy. Last the app itself. This guide shows the setup for each one. It gives you the tests.

Where the wait comes from.

TTFB is not one event. It is the whole trip. It starts when the client connects. It ends when the first byte lands on the client card.

A normal live page call has six steps.

  1. Domain Name System (DNS) lookup: The client finds the IP address. A cold lookup often costs 30 to 80 ms on a phone link.
  2. TCP three way handshake: Client and host swap three packets: SYN, SYN-ACK, and ACK. On a slow link this costs one round trip (RTT).
  3. Transport Layer Security (TLS) handshake: TLS 1.3 trades keys in one round trip (1-RTT). With TLS 1.2 it takes two round trips (2-RTT).
  4. Host request handling: Nginx takes the socket. It reads the Hypertext Transfer Protocol (HTTP) headers. It checks the routing rules. Then it hands the call to a worker.
  5. App and DB run: The app reads the call. It can be PHP-FPM, Node.js, Python WSGI, or Go. It starts its code. It runs queries against MySQL or PostgreSQL. Then it builds the HTML page.
  6. First byte send: The host flushes the HTTP headers. Then it sends the first packet of page data back to the client.
StageSlow stack (foreign host, PHP)Fast stack (Indian VPS)Time saved
DNS lookup45 ms8 ms37 ms less.
TCP handshake85 ms15 ms70 ms less.
TLS 1.3 handshake85 ms15 ms70 ms less.
Server routing and parsing25 ms2 ms23 ms less.
App and DB run380 ms4 ms376 ms less.
First byte trip (RTT)42 ms8 ms34 ms less.
Total TTFB662 ms52 ms610 ms less, 92% faster.

On a slow stack, the app and the DB take 380 ms. The TCP and TLS setup adds 212 ms more. You have to fix both layers.

What counts as a good TTFB?

In short: 200 ms or less is best. 800 ms still passes. Past 800 ms is poor.

Google's Core Web Vitals guide has three bands.

  • Good: 800 ms or less. The guide suggests a target of 200 ms or lower.
  • Between 800 ms and 1,800 ms: This is the "Needs Improvement" band.
  • Past 1,800 ms: This is the "Poor" band.

The 800 ms line is the pass mark, not a good goal. TTFB is the base of the LCP score. If TTFB is 600 ms, your browser has only 1.9 seconds left. It must fetch style sheets. It must run JavaScript. It must load fonts. Then it draws the main image. The guide calls 2.5 seconds a good LCP.

AI search bots such as OAI-SearchBot and PerplexityBot need a fast first byte too. Keep TTFB at 200 ms or less. Then these tools can read your page inside their fetch limits.

Measure your TTFB with curl.

Change nothing yet. Measure first. The curl tool shows the real split of the wait. Browser tools mix in local render time. They mix in add-ons too. So they hide the host numbers.

Create a template file named curl-format.txt. Put it on your laptop or staging box.

    time_namelookup:  %{time_namelookup}s\n
       time_connect:  %{time_connect}s\n
    time_appconnect:  %{time_appconnect}s\n
   time_pretransfer:  %{time_pretransfer}s\n
      time_redirect:  %{time_redirect}s\n
 time_starttransfer:  %{time_starttransfer}s\n
                    ----------\n
         time_total:  %{time_total}s\n

Now run curl against your page. It hides the body. It prints each timing number.

curl -w "@curl-format.txt" -o /dev/null -s "https://yourdomain.com/blog/sample-post"

The output shows the wait at each step.

    time_namelookup:  0.012140s
       time_connect:  0.034512s
    time_appconnect:  0.058291s
   time_pretransfer:  0.058380s
      time_redirect:  0.000000s
 time_starttransfer:  0.485120s
                    ----------
         time_total:  0.512390s

What each number means.

  • time_namelookup (12 ms) is the DNS lookup.
  • time_connect (34 ms) is the end of the TCP handshake. Take away the DNS time and you get the raw round trip. That leaves about 22 ms of pure network time.
  • time_appconnect (58 ms) is the end of the TLS handshake.
  • time_starttransfer (485 ms) is your exact TTFB.
  • The first byte came at 485 ms. The step before the transfer ended at 58 ms. The gap between them is 427 ms. That is 88% of the wait.
  • The network is not the problem.

Save time with a small Nginx cache.

On a live site, each guest makes the host work. That site could be WordPress. It could be Laravel. It could be a custom PHP app. The host runs PHP code. It queries the DB. That work is the main cause of a high TTFB.

A short cache (microcache) keeps the HTML in the web host. It holds a page for 1 to 10 seconds. A 5 second cache hits 95% or more of calls. This happens while traffic spikes. A cache hit turns a slow DB call into a RAM hit. That lookup takes 10 ms or less.

The Nginx cache setup.

Add these lines to the http block in your main nginx.conf.

# Define the FastCGI cache zone in shared memory
# keys_zone=WORDPRESS:64m reserves 64MB of RAM for cache keys
# max_size=2g allows up to 2GB of active cached responses on fast local storage
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=MICROCACHE:64m inactive=60m max_size=2g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout updating invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

Next, set the same rules in your site file, such as /etc/nginx/sites-available/yourdomain.conf.

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    root /var/www/html;
    index index.php index.html;

    # SSL configuration and certificates omitted for brevity

    # Determine when to bypass the microcache
    set $skip_cache 0;

    # Do not cache POST requests
    if ($request_method = POST) {
        set $skip_cache 1;
    }

    # Do not cache query strings (search forms, dynamic filters)
    if ($query_string != "") {
        set $skip_cache 1;
    }

    # Do not cache authenticated users or active carts
    if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass|woocommerce_items_in_cart") {
        set $skip_cache 1;
    }

    # Exclude administrative backends
    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|/wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
        set $skip_cache 1;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;

        # FastCGI microcaching directives
        fastcgi_cache MICROCACHE;
        fastcgi_cache_valid 200 301 302 5s;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        # Deliver stale content while an asynchronous background update runs
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_cache_background_update on;

        # Expose cache status header for telemetry and verification
        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Response-Time $request_time;
    }
}

Why the background update matters.

Without this update, a guest who hits an old cache entry has to wait. The host must build the page again. Then it can reply. That creates a slow spike. It is called a cache stampede.

Turn on fastcgi_cache_background_update on. Pair it with fastcgi_cache_use_stale updating. Nginx then sends the old copy right away. The first byte goes out fast. It takes 10 ms or less. At the same time Nginx sends a back call to PHP-FPM. That call refreshes the cache. The guest waits for nothing.

Use Redis for data.

A small cache serves guests who are not logged in. Logged in users still need live data. So do API calls. So do checkout sessions. For those, the speed of your store matters most.

A store such as MySQL or MariaDB has to parse each query. It reads the table shape. It checks the indexes. It pulls pages off disk. The same query runs again and again. It gets site options. It gets user rights. It gets post details. The wait adds up fast.

A Redis object cache keeps query results in RAM (Random Access Memory). The app asks Redis first. If the key is there, the data comes back in under 1 ms.

Use a Unix socket for Redis.

Many setups make Redis listen on 127.0.0.1:6379. That works, but each call goes through the TCP stack. It costs packet packing. It costs a switch between tasks.

A Unix domain socket on the same host skips the network stack. It cuts the talk time between programs by 20% to 30%.

Edit /etc/redis/redis.conf.

# Disable TCP listening if all consumers live on the same host
port 0

# Enable Unix domain socket
unixsocket /var/run/redis/redis-server.sock
unixsocketperm 770

# Memory management
maxmemory 512mb
maxmemory-policy volatile-lru

Give your web host and PHP-FPM user rights on that socket.

usermod -aG redis www-data
systemctl restart redis-server
systemctl restart php8.3-fpm

Check the Redis speed.

Ask Redis for its own wait time. Use the command line tool.

redis-cli -s /var/run/redis/redis-server.sock --latency

The output gives the round trip time.

min: 0, max: 1, avg: 0.08 (1240 samples)

An average of 0.08 ms shows how fast a RAM lookup is. It is far faster than a disk query. Live pages still build in 120 ms of host time or less.

Speed up the network with BBR.

Your app may build the page in 10 ms and still feel slow. That happens when the Linux stack keeps its default rules.

Cubic or BBR?

Most Linux hosts use Cubic for TCP traffic control. Cubic reads a lost packet as a sign of a full network. On weak links, radio noise and tower moves often lose packets. That is common on the 4G and 5G cell networks in India. It happens on free networks too.

When Cubic sees a lost packet, it cuts the send window by up to 30%. Speed drops. The queue builds up. No good reason.

BBR (Bottleneck Bandwidth and RTT) comes from Google. It now sits in the main Linux kernel. It does not guess from packet loss. It reads the real limit and the round trip time. It keeps full speed even with 15% random packet loss. It can send the first bytes 3x sooner on cell links.

Set the kernel values.

Create /etc/sysctl.d/99-network-tuning.conf and add these lines.

# Use Fair Queue packet scheduler required for BBR
net.core.default_qdisc = fq

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

# Enable TCP Fast Open for both incoming and outgoing connections (RFC 7413)
# Allows data payload in the initial SYN packet for returning clients
net.ipv4.tcp_fastopen = 3

# Increase connection backlog queue to absorb burst connection spikes
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192

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

# Disable slow start restart after idle
# Prevents TCP from resetting the congestion window to initial size after pause
net.ipv4.tcp_slow_start_after_idle = 0

# Increase maximum socket receive and transmit buffer sizes (up to 16MB)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# Increase maximum file descriptors system-wide
fs.file-max = 2097152

Load the new values.

sysctl -p /etc/sysctl.d/99-network-tuning.conf

Check that BBR is now in charge.

sysctl net.ipv4.tcp_congestion_control

The shell should print.

net.ipv4.tcp_congestion_control = bbr

Put the server near your guests.

A common mistake is to use a far host with a Content Delivery Network (CDN). Then the team calls the problem solved.

A CDN does cache static files at edge points of presence (PoPs). That covers images. It covers style sheets. It covers JavaScript. But a live page still has to travel. It goes to the source host. So does each query the cache does not hold.

Light in a fiber pipe sets the floor on speed.

  • US East Coast to Mumbai: 180 ms to 220 ms round trip.
  • Europe (Frankfurt) to Mumbai: 110 ms to 140 ms round trip.
  • Singapore to Mumbai: 40 ms to 55 ms round trip.
  • Indian host (Mumbai/Delhi) to Indian users: 8 ms to 28 ms round trip.

A guest in Bengaluru opens a page on a host in North America. The TCP and TLS setup alone eats 400 ms of travel time. The host has not even started the work yet.

A host in India can link to the National Internet Exchange of India (NIXI). That cuts the round trip. It stays under 25 ms in the main cities. Add a fast disk. Add an Nginx cache. The total TTFB then stays under 60 ms end to end.

See our app hosting plans for Indian compute nodes. Every plan runs in our own centre, and there is no extra layer on top.

Size the server for the traffic.

Tuned software still crawls when the host has too little power.

Give the app enough CPU cores.

Each app worker handles one call at a time. On a VPS with 1 vCPU, ten calls that miss the cache fight for one core. The wait for each call jumps from 60 ms to 600 ms. That is pure queue delay.

A site with steady crawling and real traffic needs 2 to 4 vCPUs. The spare power keeps back jobs from starving the Nginx workers.

Pick fast disk for the cache.

Disk speed decides the wait. Nginx writes cache files. MySQL reads index pages.

  • A fast local drive: 0.02 ms to 0.05 ms per random read. It reaches 500,000+ IOPS.
  • A slower shared drive: 0.2 ms to 0.5 ms per random read. It caps at about 50,000 input and output calls a second (IOPS).

Under a heavy bot crawl, long disk waits can stall the host. Read how fast local storage for Indian hosting stops these waits. It helps while traffic spikes. If you keep files or DB dumps, check our storage hosting solutions. They come with 7-day backup retention.

If you run your work in containers, read our app hosting guide. It shows the rules. Docker CPU and RAM cgroups set the limit for each service.

Common questions.

Does a CDN remove the need for tuning?

In short: No, not on its own.

A CDN edge node helps only when the call hits the cache there. A form submit goes straight to your host. So does a login. So does a personal dashboard. So does a page past its cache time. If your host takes 800 ms, the guest waits for all of it. The trip from the edge to the source adds more. Search crawlers and AI bots skip edge caches. They do it when they check for fresh content.

How is the Nginx cache different?

In short: The Nginx cache runs inside the web host. The app never starts.

Cache plugins for WordPress still need PHP to start. They read config files. They check the headers. They pull files from disk. That work costs 40 ms to 120 ms of time. The Nginx FastCGI cache lives in RAM. Nginx master and worker tasks hold it. The call stops there. PHP-FPM never runs. The app never runs. The host spends 5 ms of time or less.

In short: BBR stops a speed drop caused by radio loss.

Cell links lose packets in two cases. A phone moves between towers. The signal fades. Other noise can block it too. That is not the same as a full network. Old methods such as Cubic read it as a full network. They cut the send speed by up to 30%. BBR reads the real path. It keeps the full speed. It sends the first HTML bytes with no extra wait.

How does HTTP/3 cut the wait?

In short: HTTP/3 joins the transport and the TLS handshake. It all happens in one 0-RTT or 1-RTT exchange.

HTTP/2 sits on TCP. It needs a TCP handshake. It needs a TLS handshake. That is two round trips. HTTP/3 uses QUIC over UDP. QUIC sets the link and the TLS keys in the same step. A repeat guest sends the call in the first packet. That is zero round trips (0-RTT). Our guide on HTTP/3 and QUIC Linux VPS performance goes deeper.

In short: Fast answers stop search timeouts.

Tools such as ChatGPT Search answer a question. They search the web live. They give each page a short time. It is often 800 to 1,500 ms. Your host may need 800 ms for the first byte. The tool may then give up. It can give up before the page finishes. It then cites a site with a 200 ms answer.

Check that the changes work.

Run these tests after your changes. They tell you if each part is live.

  1. Check that BBR is active: Run sysctl net.ipv4.tcp_congestion_control. It should print bbr.
  2. Check the Nginx cache headers: Run curl -I https://yourdomain.com/blog/sample-post. The first request should show X-Cache-Status: MISS or BYPASS. The next request should show a HIT.
  3. Check the Redis socket: Run redis-cli -s /var/run/redis/redis-server.sock ping. Redis should answer PONG over the socket.
  4. Measure the TTFB many times. Run the curl -w "@curl-format.txt" test 10 times from a network outside your office. The time_starttransfer value should stay under 0.10s (100 ms).
  5. Watch the server load. Run htop or vmstat 1 during a traffic test. The central processing unit (CPU) use should stay under 10%. I/O wait (wa) should stay under 10% too. Our VPS setup guide covers the base setup.

Need help with a busy setup? Our team works month to month. You get a 30 days money-back guarantee. Message us on WhatsApp or open our contact page to talk about your host.

Written by

Renish Mithani

Hosted in India, written in English

  • 12 min read

Share this post

Need help?

Ask us on WhatsApp

Our team replies during Indian business hours. Ask us anything about hosting, and we will help. We are the people who run the servers, so we can answer the hard ones too.

That is the end of this post. If you want to keep reading, the posts below are the ones we share with it. We write every post ourselves, and each one stands on its own.

We write about hosting, servers, backups, and the day to day work of running a site for a customer. There is no sales pitch in a post, and every post is free to read.

We write them for the people who run a site from India, and we keep the words plain. We do not write for a prize, and we do not sell you anything here. Read the post, take what you need, and ignore the rest.

Related posts

We rank these by the tags they share with the post you came from. Read one to learn more. Every post is free to read.

Cookie Preferences