Performance Fundamentals: Where PHP Requests Actually Spend Time

Build a mental model of where latency comes from in a typical PHP web request, so you know what to measure and what to leave alone before you ever change a line of code.

Every performance conversation in PHP eventually turns into a debate about opcode caches, JIT compilation, or which array function is 'faster'. Almost none of that matters until you know where your actual request time is going. A typical PHP request touches CPU, memory, disk, the network, a database, and often several third-party APIs — and for most real-world applications, PHP's own execution time is the smallest slice of that pie. This lesson is the map you use before reaching for any specific tool: it explains the categories of latency you'll find in any request, why 'measure first' is not a cliché but a load-bearing engineering practice, and why the 80/20 rule usually points you at I/O and network calls rather than CPU-bound PHP code.

The anatomy of a PHP request's time budget

When a browser hits your PHP application, the total response time is the sum of many distinct phases, and each phase has a completely different cost profile. Confusing one phase for another is the single most common reason optimization effort gets wasted on the wrong thing.

It helps to think of request time as a stack of layers, each of which can dominate depending on what the request actually does. A page that renders a cached template spends almost all its time in a different layer than a page that calls three microservices and writes to two tables.

• CPU-bound work: parsing, looping, string manipulation, template rendering, encryption/hashing, image resizing done in-process. This is where the PHP engine itself (opcodes, JIT) matters most. • Memory pressure: allocating large arrays, loading big result sets or files into memory, generating large payloads. This shows up as slow garbage collection, swapping, or hitting memory_limit rather than as raw slowness. • I/O waits: reading/writing files, session storage, local cache lookups. Even 'local' disk I/O can dominate on network-attached storage or under load. • Database and network latency: query execution time plus the round-trip to reach the database, plus connection setup if pooling isn't used. On a typical CRUD endpoint this is usually the single largest line item. • Serialization overhead: encoding/decoding JSON, XML, or PHP's own serialize/unserialize format for cache values, session data, or API payloads. Cheap per-call, but it adds up linearly with payload size and call count. • External API latency: calls to payment processors, email providers, third-party data sources — each one is a full network round trip you don't control, often 50-500ms, sometimes far more under provider-side load.

Measure before you optimize

"Measure before you optimize" sounds obvious, but in practice engineers routinely skip it because a hypothesis feels obviously true — 'the JSON encoding must be slow' or 'this loop is inefficient'. Without a measurement, that hypothesis is a guess, and guesses about performance are wrong more often than intuition suggests, because modern engines, caches, and networks behave in ways that defy naive reasoning.

A measurement-first workflow looks like: reproduce the slow request, get a breakdown of where its wall-clock time went (via APM tracing, profiling, or targeted timers), identify the single largest contributor, fix only that, and re-measure. Optimizing anything else first is wasted effort even if the code you changed is technically faster afterward — if it wasn't the bottleneck, the user-facing response time barely moves.

This is also why premature micro-optimization (switching string concatenation styles, memoizing tiny pure functions) is usually a poor use of time: those savings are measured in microseconds, while a single uncached database query or one extra API round trip is measured in tens or hundreds of milliseconds. You cannot out-optimize a bad bottleneck with good code elsewhere.

The 80/20 rule: it's almost always I/O and network

For the overwhelming majority of PHP web applications — content sites, SaaS dashboards, e-commerce, internal tools — profiling data shows the same pattern again and again: 80% or more of request time is spent waiting on something outside the PHP engine itself. Database queries, cache round trips, HTTP calls to other services, and disk I/O dominate, while the PHP code gluing them together (loops, conditionals, object construction) accounts for a small remainder.

This matters because it tells you where to look first. If a request takes 400ms, and a profiler shows 350ms of that in a single database query and two sequential API calls, then optimizing your PHP array-handling code can save you a few milliseconds at best — invisible to the user. The wins that actually move the needle are: adding an index, caching a repeated query, batching N+1 queries into one, running independent API calls concurrently instead of sequentially, or avoiding an unnecessary round trip entirely.

There is a real minority of workloads where PHP CPU time genuinely dominates — heavy in-process computation like image processing, PDF generation, complex data transformation, or tight numeric loops. For those, opcode caching and the JIT (covered in the OPcache and JIT lesson) are the right lever. The skill this lesson is trying to build is not 'CPU never matters', it's 'find out which bucket your request falls into before you pick a tool'.

A minimal timing harness for spotting where time goes

hrtime(true) returns a monotonic nanosecond counter — never use microtime() for measuring durations, wall-clock time can jump. This kind of ad-hoc instrumentation is a starting point, not a replacement for a real profiler (see the Profiling PHP Applications lesson) or an APM tool once you need production-wide visibility.

<?php
// A quick, dependency-free way to see where wall-clock time is going
// inside a single request, before reaching for a full profiler.

final class Stopwatch
{
    private array $marks = [];

    public function mark(string $label): void
    {
        $this->marks[] = [$label, hrtime(true)];
    }

    /** @return array<string, float> label => milliseconds since previous mark */
    public function report(): array
    {
        $out = [];
        for ($i = 1; $i < count($this->marks); $i++) {
            [$label, $t1] = $this->marks[$i];
            [, $t0] = $this->marks[$i - 1];
            $out[$label] = ($t1 - $t0) / 1_000_000; // ns -> ms
        }
        return $out;
    }
}

Code Examples

This deliberately mixes the four common categories of request time — CPU, database, serialization, and an external API call — and times each phase separately. Running this against real traffic patterns almost always reveals that db_query and external_api dwarf cpu_math and serialize, which is the 80/20 rule in action. Expected result: cpu_math 4.10 ms db_query 18.70 ms serialize 0.30 ms external_api 142.50 ms In this run, the external API call alone is roughly 8x the database query and 35x the CPU-bound math. No amount of optimizing the array_map/array_sum line would have a visible effect on total response time — the fix here would be caching the shipping rate, calling it asynchronously, or giving it a tighter timeout with a fallback.

foreach ($watch->report() as $label => $ms) {

<?php
require __DIR__ . '/Stopwatch.php';

$watch = new Stopwatch();
$watch->mark('start');

// 1. CPU-bound work: building a report from an in-memory dataset.
$numbers = range(1, 200_000);
$sumOfSquares = array_sum(array_map(static fn (int $n): int => $n * $n, $numbers));
$watch->mark('cpu_math');

// 2. Database round trip.
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop', 'app', 'secret', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->query('SELECT id, name, price FROM products ORDER BY created_at DESC LIMIT 50');
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$watch->mark('db_query');

// 3. Serialization: turning the result set into a JSON payload.
$json = json_encode(['sum' => $sumOfSquares, 'products' => $products], JSON_THROW_ON_ERROR);
$watch->mark('serialize');

// 4. External API call (e.g. fetching live shipping rates).
$ch = curl_init('https://api.example-shipping.test/v1/rates?zip=94107');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
$rates = curl_exec($ch);
curl_close($ch);
$watch->mark('external_api');

foreach ($watch->report() as $label => $ms) {
    printf("%-14s %8.2f ms\n", $label, $ms);
}

Why this mental model matters in production

Teams that adopt 'measure the whole request, then fix the biggest piece' consistently ship faster performance wins than teams that start from 'PHP is slow, let's tune the engine'. A classic real-world pattern: a checkout page feels sluggish, someone assumes it's PHP template rendering and adds output caching, but a trace shows three sequential, uncached calls to a fraud-detection API account for 90% of the time. Running those calls concurrently (or caching the low-risk ones) fixes the page in an afternoon; the template caching effort would have shaved off single-digit milliseconds. The lesson generalizes: always find the actual largest slice of the pie before deciding which tool — OPcache tuning, FPM capacity planning, or a profiler — to reach for.

// "This loop looks inefficient, let me rewrite it" foreach ($items as $item) { $total += $item->getPrice(); } // ...ships a micro-optimized version without ever measuring the request

$watch->mark('before_loop'); foreach ($items as $item) { $total += $item->getPrice(); } $watch->mark('after_loop'); // then check $watch->report() alongside the DB and API timings // before deciding this loop is even worth touching

Optimizing code because it 'looks slow' without measuring is the most common way engineering time gets wasted on performance work. A tight PHP loop over a few hundred objects is almost always negligible next to a single uncached query or API call; changing it first treats a symptom you haven't confirmed exists.

For a typical PHP web application (not heavy in-process computation), where does the majority of request time usually go? A. Raw PHP CPU execution such as loops and string handling B. I/O and network latency: database queries, external API calls, and similar round trips C. Garbage collection of PHP objects D. Opcode compilation of the PHP source on every request

B. I/O and network latency: database queries, external API calls, and similar round trips — The 80/20 rule for most PHP applications is that I/O and network latency — database round trips, external API calls, cache lookups — dominates request time, while PHP's own CPU execution is usually a small fraction of the total.

[true-false] It's good practice to optimize code that 'looks slow' immediately, without first measuring where request time is actually spent. A. True B. False

B. False — False — optimizing before measuring risks spending effort on code that isn't the bottleneck. Always measure the full request first, identify the largest contributor, and fix that.

Which of these is an example of serialization overhead in a PHP request? A. Waiting for a MySQL query to return rows B. Encoding a PHP array into JSON before sending an HTTP response C. The TCP handshake with a remote API D. PHP-FPM spawning a new worker process

B. Encoding a PHP array into JSON before sending an HTTP response — Serialization overhead refers to converting data between representations, such as encoding an array to JSON or XML, or using serialize()/unserialize() for cache and session storage.

Take an endpoint in your own application and add Stopwatch-style marks around each major phase (input parsing, database access, business logic, external calls, response serialization). Run it under normal load and rank the phases by milliseconds. Which single phase, if halved, would produce the largest improvement in total response time?

For most CRUD-style endpoints the ranking is typically: external API / database time > serialization > in-process CPU work, confirming that I/O and network calls are the highest-leverage place to focus first.

Don't guess — add the marks, run the request several times to smooth out noise, and look at the numbers before forming a theory.