Performance and Caching
Optimisation does not start with code — it starts with a measurement. Intuition lies here almost every time: the candidate who "feels" where the slowness is fixes the wrong place and then explains why it did not help much. A profiler answers the only question that matters — where the request's time actually goes. And it almost always turns out not to go into PHP at all: the interpreter computes fast, while the seconds pile up in the database, on the network, and in blocking calls to somebody else's service. One query repeated three hundred times in a loop; one heavy aggregate with no index; one curl_exec() to a service that today answers in two seconds — that is what those three seconds of response time are usually made of.
Everything in this topic follows from that. A cache removes work you can avoid redoing; a queue removes work you do not have to do right now; scaling adds capacity when there is nothing left to remove. Let us name the traps upfront, because this is where most candidates fall. OPcache is a cache of compiled opcodes, not a data cache: it holds neither your query results nor your rendered output, and turning it on makes a slow query no faster. JIT helps CPU-heavy code, while a typical web application is bound by I/O and the database — so "enable JIT to speed up the API" is not an answer. APCu and a static property live inside one FPM process, not across all of them. A TTL is a safety net, not an invalidation strategy. A queued job never runs itself until a separate worker picks it up. And degradation that appears only under load is usually pool saturation, not slow code. The layer-by-layer breakdown is below.
Topic map
- Profiling — Xdebug and Blackfire, reading the call graph by inclusive wall time, and why N+1 is caught by a query counter rather than a profiler.
- Application cache, OPcache and JIT — what OPcache really caches, how Redis and Memcached differ from it, why APCu is not shared, and who JIT actually helps.
- Cache invalidation and cache-aside — the cache-aside pattern, key design, deleting instead of updating, tags and version prefixes, and guarding against a stampede.
- HTTP caching —
Cache-Controland freshness,ETagand304,publicversusprivate, and why anETagsaves the transfer but not the work. - CDN and static assets — what moving assets off takes away from the origin, why file names must be versioned, and what a CDN will not do for you.
- Queues and background jobs — why an inline call holds a worker, how Laravel queues and Symfony Messenger are built, idempotency and retries.
- Scaling PHP-FPM — the worker pool as a concurrency ceiling, sizing
pm.max_childrenfrom RAM, shared-nothing, and diagnosing pool saturation.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Believing OPcache caches your data or your rendered output | It holds compiled opcodes only — a slow database query gets no faster from it |
Enabling JIT to speed up a web API | A web workload is bound by I/O and the database, not the CPU — the gain is near zero |
Treating APCu or a static property as a shared cache | They live inside one FPM process — the next worker simply has no cache |
| Hunting N+1 with a profiler | 300 queries of 1 ms never surface as one slow call — you count them, you do not time them |
| Building a cache key without the filters, page and locale | Two different queries collide on one entry and a user is served somebody else's result |
| Leaning on a TTL as the invalidation strategy | Stale data is served for the entire lifetime of the key |
| Leaving a cold key unguarded | A burst of concurrent misses rebuilds it all at once and takes the database down |
Treating Redis as a dependency rather than a cache | A cache outage instantly becomes an application outage |
Sending a long public max-age on a per-user page | A shared proxy stores the response and hands it to a different user — a real data leak |
Computing the ETag after rendering the whole page | You saved the transfer but not the work — PHP still built the entire response |
| Shipping new assets under the same file name | The CDN edge node keeps serving the stale copy after a deploy |
| Queueing a job and never running a worker | The job sits in the queue and never executes — a queue starts nothing by itself |
| Not restarting the queue worker on deploy | A long-lived process keeps the old code in memory and runs it |
| Treating at-least-once delivery as exactly-once | A retry sends the email a second time — the handler must be idempotent |
Sizing pm.max_children from the CPU count | The box swaps and ends up slower than it was before the "optimisation" |
Raising pm.max_children while a worker is blocked on an external call | The queue merely moves inside the pool — you need a timeout and the call taken out of the request |
Interview relevance
Performance is where an interviewer quickly tells apart someone who has carried production load from someone who has read the articles. What is probed is not a list of technologies but your order of operations and the mechanism: what you start with (a measurement), what exactly you cache, what happens to the cache on a write, and what happens when the cache node dies. A candidate who says "I'll put Redis in front of it" without explaining the key, the invalidation and the miss path is answering a different question.
Typical checks:
- What
OPcachecaches and how it fundamentally differs fromRedisandMemcached. - Why
APCuandstaticare a per-worker cache rather than a shared one, and what survives a request in PHP at all. - Who
JIThelps and why an ordinary web application is not that. - How you profile a slow endpoint and how you tell N+1 apart from one heavy query.
- How a cache key is designed and how a write invalidates the affected entries.
- What a stampede is and what you guard it with.
- What
Cache-ControlandETagdo, and the difference betweenpublicandprivate. - Why a queue, who consumes it, and why the handler must be idempotent.
- How
pm.max_childrenis calculated and what pool saturation looks like.
Common wrong answer: "Turn on OPcache — it will cache the data and the queries will get faster." That is the headline mistake of the whole topic: OPcache caches the opcodes of your PHP files, which removes parsing and compilation from every request — and precisely nothing else. It does not hold the result of your SQL query, it does not hold rendered HTML, and it does not make a slow query fast. The data cache is Redis or Memcached — a separate store, shared across every worker, into which you put what your code computed. Conflating the two is the single most common failure in this interview, and the runner-up follows right behind it: answering "let's enable JIT" to a question about a slow API that in reality spends nearly all its time waiting for the database.