Caching Quick Wins for LAMP Applications

Most LAMP performance problems we see are not exotic. They are a missing cache at one of three layers — opcode, application, HTTP — or a cache someone believes is working and is not. Here are the wins that take hours, not weeks, roughly in order of payoff per effort.

First, measure something

Before touching configuration, get one number you trust: p95 response time for the two or three routes that matter, from the access log. awk over Apache's %D field is enough. Every change below should move that number or be reverted. Tuning without a baseline is superstition.

OPcache: the free win most servers half-use

OPcache ships with every modern PHP and caches compiled bytecode so PHP stops re-parsing your entire codebase on every request. It is usually enabled — and usually mis-sized.

Check three things with opcache_get_status() (or a one-line status script):

  • opcache_hit_rate should be above 99% on a warm production server. Lower means restarts or memory pressure.
  • memory_usage: if free_memory is near zero, raise opcache.memory_consumption (128–256 MB covers most monoliths). When OPcache fills, it silently restarts itself and your hit rate craters on a schedule.
  • opcache.max_accelerated_files must exceed your file count (find . -name '*.php' | wc -l). It rounds up to a prime; leave headroom.

On production, set opcache.validate_timestamps=0 so PHP never stats files on the hot path — with the understanding that deploys must reload PHP-FPM to pick up new code. If your deploy tooling cannot guarantee that, leave validation on with opcache.revalidate_freq=60 and take the smaller win.

The MySQL query cache is not saving you

If you are on MySQL 5.7 with query_cache_size set, know two things: any write to a table invalidates every cached query touching that table, so on write-heavy workloads the cache costs more than it returns; and MySQL 8.0 removed it entirely. Stop counting on it now. If a read-heavy query is hot enough to need caching, cache it in the application where you control invalidation.

Application-layer caching with Redis

One Redis instance next to the application covers three distinct jobs:

  • Sessions. Moving PHP sessions from files to Redis (session.save_handler=redis) removes file locking, which serializes concurrent requests from the same user — the classic "the site hangs when I open two tabs" bug — and makes a second web server possible later.
  • Computed fragments. The expensive homepage query, the category tree, the settings rows read on every request: cache the result with a TTL measured in minutes. A 60-second TTL on one hot query has rescued more than one launch day.
  • Rate limiting and locks, when you need them, live naturally in the same place.

Keep keys namespaced and give everything a TTL. A cache with no expiry is a data store you did not mean to build.

HTTP: let browsers and Apache do their share

Two Apache modules pay for themselves in minutes:

  • mod_deflate (or brotli): compress HTML, CSS, JS, JSON. Typical payloads shrink 60–80%.
  • mod_expires / Cache-Control: static assets should ship Cache-Control: max-age of at least a week — with content-hashed filenames if the build produces them, or a version query string if it does not. Every asset request a browser does not make is the cheapest request you will ever serve.

If the site has anonymous read-heavy pages, full-page caching (Varnish, or a CDN in front) is the biggest lever of all — but it is a project, not a quick win, because of cookies and cache invalidation. Do the four things above first; they often make the bigger project unnecessary.

What not to cache

Do not cache authenticated pages at the HTTP layer without understanding cookie behavior — serving one user's account page to another is the failure mode. Do not cache anything you cannot regenerate. And resist caching over a slow query you have not looked at: an EXPLAIN and one index frequently beats the cache you were about to build, and it never serves stale data.

The pattern across all of these: caching is not one feature, it is a layer at each tier, and each layer needs an eviction story you can explain in one sentence. If you cannot say when and how an entry leaves the cache, you have not finished the job.