Nginx in Front of Bitrix: Five Config Lines That Cut TTFB by 600ms
Most Bitrix deployments have TTFB above 700ms. Not because PHP is slow. Because the Nginx config is the one the hosting provider dropped in place years ago and nobody's touched since.
I check this in the first hour on every new project. Almost always, it's the same story.
The TTFB problem most Bitrix deployments ignore
TTFB — time to first byte — is what Google uses as a Core Web Vitals signal. Under 600ms is considered good. Everything above that costs you ranking, especially in e-commerce where competition for catalog pages is high.
On a 28k SKU project, we were seeing median TTFB of 1.1s. PHP-FPM itself was responding in 480ms. That left 620ms unaccounted for inside Nginx — not in the database, not in code. In a config that nobody had touched in three years.
After fixing it, median TTFB dropped to 480ms. LCP went from 2.9s to 2.1s. No PHP changes.
Why the default Nginx config is the wrong starting point
Standard Bitrix deployment: browser → Nginx → PHP-FPM → MySQL → response back.
Nginx here is a reverse proxy. It accepts connections, passes requests to PHP-FPM over FastCGI, receives the response, returns it to the browser.
The problem with the default config: every request wakes PHP. The catalog page you've loaded fifteen times gets processed from scratch each time. Static files hit the filesystem on every request. The FPM connection opens and closes per request.
It works. It's just slow.
Five parameters that change this.
fastcgi_cache: serving pages without waking PHP
fastcgi_cache is an Nginx module that stores the PHP-generated HTML response on disk, serving subsequent requests directly from cache without calling PHP-FPM. It's the highest-impact change in this list.
At peak load, this cuts PHP requests by 3–5x on catalog pages. For pages that change infrequently, it's the right tool.
Basic setup:
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=bitrix_cache:64m inactive=10m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 10m;
fastcgi_cache_valid 404 1m;
fastcgi_cache bitrix_cache;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
$no_cache is a variable you build from conditions. More on that below.
One thing worth knowing: fastcgi_cache_valid 200 10m means cached catalog pages live for 10 minutes. If a product updates, the cache is stale until it expires. For most catalogs that's fine. For real-time pricing pages — it's not.
Buffers, open_file_cache, and keepalive: the settings everyone ignores
open_file_cache eliminates repeated stat() syscalls for static files — CSS, JS, images.
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
Without it, every static request checks the filesystem. With it, Nginx caches file descriptors. On a site with 200–400 unique static assets, the difference shows up in RPS under load, not in TTFB directly.
Buffers matter too. Default fastcgi_buffer_size 4k and fastcgi_buffers 8 4k are built for small responses. A Bitrix catalog page easily runs 50–80KB. When the response doesn't fit in the buffer, Nginx writes it to disk and reads it back — adding 30–60ms per request.
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_busy_buffers_size 64k;
For keepalive connections to PHP-FPM:
upstream php_fpm {
server 127.0.0.1:9000;
keepalive 32;
}
And in the location block:
fastcgi_keep_conn on;
By default, Nginx opens a new TCP connection to PHP-FPM for every request. At 500+ RPS, that's 5–15ms of overhead per request — invisible in a profiler until you're looking at it. keepalive lets Nginx reuse connections. When PHP-FPM is local (127.0.0.1), the gain is modest. When PHP is on a separate server, it's more noticeable.
Bitrix-specific cache exclusions you cannot skip
Without the right exclusions, fastcgi_cache breaks authentication, the cart, and all dynamic requests.
The $no_cache variable I mentioned assembles from multiple conditions:
map $request_uri $no_cache_uri {
default 0;
~*/bitrix/ 1;
~*/local/ 1;
~*/cart/ 1;
~*/personal/ 1;
~*/order/ 1;
~*/checkout/ 1;
}
map $http_cookie $no_cache_cookie {
default 0;
~*BITRIX_SM_LOGIN 1;
~*PHPSESSID 1;
}
set $no_cache 0;
if ($request_method = POST) { set $no_cache 1; }
if ($no_cache_uri) { set $no_cache 1; }
if ($no_cache_cookie) { set $no_cache 1; }
if ($http_x_requested_with = "XMLHttpRequest") { set $no_cache 1; }
The logic: if a user is logged in (cookie BITRIX_SM_LOGIN present), their requests go straight to PHP. Cart, account pages, AJAX — always bypass cache.
Check this manually after setup. Walk through the site as an authenticated user and as an anonymous visitor. The cart should work correctly for both.
Before and after: real numbers
On the 28k SKU project, after applying all five changes:
- Median TTFB: 1.1s → 480ms
- Peak TTFB: 1.8s → 0.9s
- LCP: 2.9s → 2.1s
- PHP-FPM load at peak: dropped roughly 40% as the cache absorbed catalog requests
A few things that don't work or complicate the picture:
If your frontend already uses Next.js ISR, adding fastcgi_cache creates two independent cache layers with separate TTLs. Invalidation gets unpredictable. Pick one.
keepalive to PHP-FPM won't help if the FPM pool has an insufficient pm.max_children limit (see the PHP-FPM post). Requests queue regardless of keepalive. Sequence matters: fix FPM first, then tune Nginx.
open_file_cache with very frequent deploys — if you're deploying every few minutes, drop open_file_cache_valid to 5–10 seconds or disable it. Otherwise Nginx serves stale static files.
An hour of config work, results visible in WebPageTest immediately. If the PHP-FPM pool is already configured (php-fpm-pool-bitrix-highload) and the core PHP issues are closed (bitrix-not-slow-its-you), the Nginx layer is the last uncovered optimization left.