<< Home

Chapter 19: Performance and Deployment

Performance comes from efficient code, well tuned configuration, and good caching at every level. Deployment builds on that foundation by providing repeatable, isolated environments and clear monitoring. This chapter shows how to configure OPcache, profile bottlenecks, introduce caching layers, prepare production settings, containerize PHP, and observe your system in real time.

💡 Measure before optimizing. Guesswork often wastes effort, but profiling focuses attention where it matters.

OPcache and Realpath Cache

PHP normally parses and compiles every script on each request. OPcache stores compiled bytecode in memory to remove this overhead. Realpath cache remembers file path resolutions so repeated includes avoid filesystem lookups.

Enabling and tuning OPcache

Enable OPcache in php.ini and adjust its limits for your project size. Increase the memory size if you have many scripts and raise interned strings buffer for large frameworks.

; opcache.ini
zend_extension=opcache
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1

Disabling validate_timestamps avoids checking file modification times on each request. Reload the service after deployment instead.

Clearing OPcache safely during deployments

When you deploy new code, invalidate OPcache so the next request reloads scripts. You can call opcache_reset() in a maintenance script or reload the PHP-FPM pool.

// clear_opcache.php
if (function_exists('opcache_reset')) {
  opcache_reset();
}
⚠️ Never clear OPcache on every request. That removes the benefit entirely and can even slow performance compared to no cache at all.

Tuning the realpath cache

The realpath cache remembers directory lookups and symbolic links. Set larger limits for big frameworks with deep include trees.

; php.ini
realpath_cache_size=4096K
realpath_cache_ttl=600

Profiling and Bottlenecks

Profiling shows where time and memory are consumed. Collect data under realistic loads and use tools that visualize call graphs and memory allocation. Optimize high impact paths, not random code.

Using Xdebug profiler for local analysis

Xdebug can record function call times into cachegrind files viewable with tools like QCacheGrind or Webgrind. Enable only when profiling, since it adds overhead.

; php.ini
xdebug.mode=profile
xdebug.output_dir=/tmp/profiles

Using xhprof and tideways_xhprof

For lower overhead profiling, use xhprof or tideways_xhprof. They sample call times with minimal impact, making them suitable for staging or short bursts in production.

xhprof_enable();
... // your app logic
$data = xhprof_disable();
file_put_contents('/tmp/profile.json', json_encode($data));

Reading call graphs and focusing optimizations

Call graphs reveal which functions dominate runtime. Optimize those first: reduce I/O, cache repeated computations, and precompute constants. Micro-optimizations like string concatenation order rarely help compared to reducing query counts or external API calls.

💡 Measure again after each change. Only commit optimizations that produce measurable wins under realistic conditions.

Caching Layers

Caching removes redundant work. PHP projects typically layer caches: opcode, data, application, and HTTP. Each layer shortens a different path between request and result.

Using APCu for in-memory data caching

APCu stores simple key-value pairs in memory for one process pool. It suits shared configuration, computed templates, or small results reused often.

if (apcu_exists('config')) {
  $config = apcu_fetch('config');
} else {
  $config = load_config_from_db();
  apcu_store('config', $config, 300);
}

Adding shared caches with Redis or Memcached

Distributed caches work across processes or servers. They reduce load on databases and improve response times for frequent reads.

$redis = new Redis();
$redis->connect('redis', 6379);

$key = 'user:' . $id;
if ($cached = $redis->get($key)) {
  return unserialize($cached);
}

$user = $repo->find($id);
$redis->setex($key, 600, serialize($user));

Leveraging HTTP and reverse proxies

Static and dynamic pages can benefit from edge caching. Set response headers like Cache-Control, ETag, and Last-Modified. Reverse proxies such as Varnish or Cloudflare cache responses and serve them directly when valid.

header('Cache-Control: public, max-age=300');
header('ETag: "abc123"');
⚠️ Never cache personalized content without proper user segmentation. Separate caches per session or authorization token.

Configuration for Production

Production configuration prioritizes stability, predictable performance, and controlled logging. Keep detailed error output off, isolate logs, and disable modules you do not use.

Setting secure and consistent defaults

Disable features not needed in production and restrict file uploads. Enable OPcache, set timeouts, and avoid high memory use per process.

; production.ini
display_errors=0
log_errors=1
error_log=/var/log/php/error.log
expose_php=0
file_uploads=1
upload_max_filesize=10M
post_max_size=12M
max_execution_time=30
max_input_time=60
memory_limit=256M
opcache.enable=1

Separating configuration by environment

Store different .ini sets or use environment variables. The phpinfo() page can confirm which files are active but never leave it public on production servers.

💡 Keep configuration under version control but exclude secrets. Combine base settings with small environment overrides.

Docker and Containerized PHP

Containers isolate dependencies and environment settings, ensuring consistent behavior from laptop to production. Docker allows reproducible builds and simplified scaling when combined with orchestration tools.

Creating a minimal PHP-FPM image

Start with an official base image and install only the extensions your app needs. Use multi-stage builds to keep the final image small.

# Dockerfile
FROM php:8.3-fpm-alpine AS base

RUN docker-php-ext-install pdo_mysql opcache

WORKDIR /var/www/html
COPY . .

FROM base AS prod
ENV APP_ENV=production
CMD ["php-fpm"]

Running with Nginx using Docker Compose

Compose files define multiple services and their networks. Connect php-fpm with Nginx through a shared volume and network.

# docker-compose.yml
version: '3.9'
services:
  app:
    build: .
    volumes:
      - .:/var/www/html
    networks:
      - backend

  web:
    image: nginx:1.27-alpine
    volumes:
      - .:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
    networks:
      - backend

networks:
  backend:

Configuring PHP-FPM and health checks

Expose health endpoints and tune process settings. For small sites pm = dynamic works well; for APIs under load consider pm = static with fixed workers.

; www.conf
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.status_path = /status
⚠️ Never build secrets into the image. Pass them as environment variables or use a secrets manager at runtime.

Observability and Error Monitoring

Observability turns unknowns into data. Instrument logs, metrics, and traces so you can detect issues before users report them. Choose lightweight libraries that integrate with your framework and centralize outputs.

Structured logging with Monolog

Structured logs include context, timestamp, and level. Use handlers to send logs to files, syslog, or external services. Format logs as JSON for ingestion by tools such as ELK or Loki.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/../var/app.log', Logger::INFO));

$log->info('User login', ['user' => $id]);

Adding metrics and health endpoints

Expose application metrics in a simple format like Prometheus text. Track request duration, error counts, and cache hits. Add a lightweight /health route that returns 200 OK when dependencies respond.

header('Content-Type: text/plain');
echo "requests_total 1234\n";
echo "errors_total 12\n";

Using error monitoring services

Tools like Sentry or Bugsnag capture uncaught exceptions and contextual data. Install their SDKs and initialize early in your bootstrap.

Sentry\init(['dsn' => getenv('SENTRY_DSN')]);

try {
  // app code
} catch (Throwable $e) {
  Sentry\captureException($e);
  throw $e;
}
💡 Collect metrics, traces, and logs in one dashboard. Triangulating across them shortens investigation time and clarifies system behavior.

Efficient caching, tuned configuration, reliable containers, and thorough observability yield fast and predictable deployments. Combined with testing and security practices from earlier chapters, these complete the core of a robust PHP production stack.