Originally published at hamzaahmadaslam.com.
How this article was made: drafted with AI assistance from a researched brief, then checked against primary documentation before publishing.
Short answer: To reduce WordPress TTFB, measure the same request at each layer instead of tuning WordPress blindly. Separate DNS, connection and TLS time from time to first byte, compare CDN hits with origin requests, then profile PHP, database work and external calls on the slow path.
How to reduce WordPress TTFB with repeatable tests
TTFB is elapsed time from navigation start until the first response byte arrives. The web.dev TTFB guide includes redirects, DNS, connection setup, TLS, the request, server processing and the trip back to the client. Its current rough guide is that most sites should strive for 0.8 seconds or less.
That is a user-facing guideline, not a PHP budget. Server response time is only one part of the measured path, and a fast edge cache can hide a slow origin.
Separate the request phases with curl
Start with one URL, one test location and one cache state. This command discards the body and prints cumulative curl timings:
curl -sS -o /dev/null \
-w 'DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n' \
https://example.com/
Example output, with illustrative values only:
DNS: 0.018421s
Connect: 0.052884s
TLS: 0.091736s
TTFB: 0.284913s
Total: 0.329442s
The values are cumulative from the start of the transfer. time_namelookup ends after name resolution. time_connect ends after the transport connection. time_appconnect ends after the TLS handshake for HTTPS. time_starttransfer ends when curl receives the first byte.
Subtract adjacent values for an approximate phase cost. The gap from TLS completion to first byte still includes request transmission, edge or origin processing, and return transit. It is not pure PHP time.
Repeat the same command several times. Keep individual runs so intermittent queueing remains visible.
Compare cached and uncached requests
Warm an anonymous public URL, then rerun the same request and inspect its cache headers. For an origin-path comparison, use a logged-in test session when that state normally bypasses full-page cache:
curl -sS -b cookies.txt -o /dev/null \
-w 'DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n' \
https://example.com/
You can also try a query string such as ?ttfb_probe=1, but do not assume it bypasses cache. CDN and page-cache rules can include, ignore or separately cache query strings. Verify the result from the response headers.
Keep public and personalized requests separate. Anonymous and logged-in WooCommerce requests can take different execution paths.
How to tell edge time from origin time
A CDN hit measures the edge path. A miss or bypass usually includes the origin path too. Compare both before changing PHP or database settings.
Read CDN cache status headers
Dump only the response headers first:
curl -sS -D - -o /dev/null https://example.com/
Cloudflare uses cf-cache-status. HIT means Cloudflare served its cache. MISS means it fetched from origin. BYPASS and DYNAMIC are not cache hits. Other CDNs and proxies may expose x-cache style headers.
Age reports how long a response has been stored in a proxy cache. Record the provider-specific CDN cache status beside every timing run.
Example output, with illustrative values only:
HTTP/2 200
cf-cache-status: HIT
age: 84
If the public page is slow only on a miss, focus on the origin path. If a confirmed edge hit is still slow, inspect client-to-edge DNS, connection, TLS and routing before touching WordPress.
Test the origin directly when access allows it
If the origin accepts direct traffic for the production hostname, curl can pin that hostname to the origin IP while preserving the URL host and TLS name:
curl -sS --resolve example.com:443:ORIGIN_IP -o /dev/null \
-w 'TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n' \
https://example.com/
Replace ORIGIN_IP with the actual origin address. Do not disable certificate validation to make this test pass. If the origin firewall accepts only CDN traffic, run an equivalent test from an authorized network or use host-level monitoring.
Compare a CDN hit, miss and direct-origin request with the same application state. The gap points to the layer that needs the next trace.
How to expose server time with Server-Timing
The Server-Timing response header lets browser DevTools display server-side metrics beside the network request. A small must-use plugin can mark WordPress bootstrap stages without modifying the theme.
Create wp-content/mu-plugins/ttfb-server-timing.php:
<?php
/**
* Plugin Name: TTFB Server Timing
*/
global $ha_ttfb_start, $ha_ttfb_marks;
$ha_ttfb_start = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
? (float) $_SERVER['REQUEST_TIME_FLOAT']
: microtime( true );
$ha_ttfb_marks = array(
'bootstrap' => ( microtime( true ) - $ha_ttfb_start ) * 1000,
);
function ha_ttfb_mark( $name ) {
global $ha_ttfb_start, $ha_ttfb_marks;
$ha_ttfb_marks[ $name ] = ( microtime( true ) - $ha_ttfb_start ) * 1000;
}
add_action( 'plugins_loaded', static function () {
ha_ttfb_mark( 'plugins' );
}, 999 );
add_action( 'after_setup_theme', static function () {
ha_ttfb_mark( 'theme' );
}, 999 );
add_action( 'init', static function () {
ha_ttfb_mark( 'init' );
}, 999 );
add_action( 'wp_loaded', static function () {
ha_ttfb_mark( 'wp_loaded' );
}, 999 );
add_filter( 'wp_headers', static function ( $headers ) {
global $ha_ttfb_start, $ha_ttfb_marks;
if ( ! current_user_can( 'manage_options' ) ) {
return $headers;
}
$previous = 0.0;
$metrics = array();
foreach ( $ha_ttfb_marks as $name => $cumulative ) {
$metrics[] = sprintf( '%s;dur=%.1f', $name, $cumulative - $previous );
$previous = $cumulative;
}
$to_headers = ( microtime( true ) - $ha_ttfb_start ) * 1000;
$metrics[] = sprintf( 'to_headers;dur=%.1f', $to_headers - $previous );
$headers['Server-Timing'] = implode( ', ', $metrics );
return $headers;
}, 999 );
The bootstrap mark covers what ran before must-use plugins loaded, including wp-config.php, the database connection and any object cache drop-in. The code emits the header only for users who can manage_options, limiting exposure of internal timing data. DevTools will show the metrics on that authenticated request.
These marks stop when WordPress prepares its main headers. They do not measure PHP work after that point. They also do not include time a request spent waiting for a free PHP-FPM worker. Pair them with Query Monitor and PHP-FPM diagnostics.
How to profile WordPress after the request reaches PHP
Install Query Monitor on a safe test or controlled production session. Its official plugin documentation covers database queries, PHP errors, hooks and actions, HTTP API calls, and grouping database queries by the responsible plugin, theme or function.
Start on the exact slow URL and user state. Check database time, slow or repeated queries and their callers. Review HTTP API calls that block the response, then use hook and component data to tie work to code.
Disable the profiler for final curl comparisons because profiling itself adds work.
How to confirm the usual causes before fixing them
Missing or bypassed page cache
Confirm page caching with response headers, not a settings screen. Repeated misses on an eligible public URL need investigation.
Fix the cache key, eligibility rule, purge behavior or page-cache layer identified by the trace. Do not force public HTML caching onto personalized account, cart or checkout responses.
Slow database queries
Use Query Monitor to confirm database time on the slow dynamic request and identify each expensive query's caller. Remove repeated lookups or unnecessary metadata scans first. Add an index only when the actual query and its execution plan justify it.
A large autoloaded options set
Since WordPress 6.6, Site Health includes an autoloaded-options size test. The current site_status_autoloaded_options_size_limit reference documents a default warning threshold of 800,000 bytes.
Confirm ownership before changing an option. Remove only verified stale data, and stop autoloading large values not needed on most requests.
No persistent object cache
WordPress's built-in object cache is non-persistent, so cached objects normally disappear after each request. If repeated dynamic traces show the same cacheable lookups, add a supported persistent object-cache backend and measure database time again. Page cache and object cache solve different problems.
Slow external API calls on the request path
Query Monitor exposes WordPress HTTP API calls and timing. If a remote call blocks the pre-header path, cache stable remote data when allowed or move nonessential work outside the user-facing request. For required calls, define timeout and failure behavior deliberately.
Saturated PHP-FPM workers
pm.max_children limits the number of simultaneous requests a PHP-FPM pool can serve. The PHP-FPM status page exposes the listen queue, active and idle processes, max children reached, and slow-request counters.
Enable a status path in the relevant pool:
pm.status_path = /fpm-status
Map that path to the FPM socket and restrict it. The PHP manual shows this Apache pattern:
<LocationMatch "/fpm-status">
Require local
ProxyPass "unix:/var/run/php-fpm.sock|fcgi://localhost/"
</LocationMatch>
The socket path must match your deployment. Then read the status locally:
curl -sS 'http://127.0.0.1/fpm-status?full'
Example output, with illustrative values only:
pool: www
process manager: dynamic
listen queue: 2
idle processes: 0
active processes: 8
total processes: 8
max active processes: 8
max children reached: 3
slow requests: 4
A nonzero listen queue during the incident means requests are waiting. A rising max children reached counter confirms the pool has hit its worker limit since startup. If saturation blocks the normal status URI, PHP also documents pm.status_listen for a separate status listener.
Enable the FPM slow log in the same pool with a threshold chosen for the request you are investigating:
request_slowlog_timeout = 1s
slowlog = /var/log/php-fpm/www-slow.log
PHP documents 0 as the default for request_slowlog_timeout, which disables slow-request tracing. The 1s value above is an illustrative capture threshold, not a universal target. Use the emitted PHP backtraces to find the blocked code path before raising pm.max_children.
More workers consume more memory, CPU and database capacity. Raising the limit without checking those resources can move the queue elsewhere. Use the guide to sizing WordPress PHP workers from memory per process before changing worker counts.
OPcache disabled or undersized
Check OPcache from the same FPM SAPI that serves WordPress. A CLI php -i result can describe a different SAPI and configuration file, so it is not enough to prove what FPM is using.
A protected temporary diagnostic using opcache_get_status(false) can show whether OPcache is enabled, cache fullness, memory use and restart counters. Remove it after diagnosis. If the cache fills or restarts for lack of space, size it from observed use instead of copying a fixed value.
Redirect chains
Redirects add to navigation TTFB before the final response starts. Follow the chain and print every response header block:
curl -sS -L -D - -o /dev/null https://example.com/
Point internal links at the canonical HTTPS URL and consolidate avoidable redirect hops. Recheck login, checkout, language and canonical-host behavior afterward.
How to verify each change
Change one layer at a time. Run the same curl timing, from the same test location, against the same URL and user state. Record the cache status with the timing so a CDN hit is never compared casually with an origin miss.
For application changes, compare the same Query Monitor view or Server-Timing markers. For PHP-FPM, compare queue and worker counters under equivalent load.
Retest the real browser path. TTFB is not a Core Web Vital, but slow TTFB can delay later loading milestones.
What to do next
If dynamic requests still repeat database reads, follow the Redis object cache guide. If Site Health flags autoloaded data, use the autoloaded options guide before changing rows.
Once TTFB is stable, set broader page targets with the free Core Web Vitals budget calculator. If the bottleneck spans CDN, PHP-FPM and database layers, the WordPress performance optimization service covers diagnosis, implementation and verification.
Top comments (0)