Back to blog

Performance

PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained

Introduces RoadRunner as a Go-based PHP app server, covering worker loops, middleware, HTTP/2, and benchmarks vs php-fpm.

  • PHP
  • RoadRunner
  • Microservices
  • Performance
  • Workers

Reader map

Key points in PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained

Syntax first, runtime behavior second, migration cleanup last.

Read
16 min
Waypoints
4
Track
Performance
  1. 01
    Start here

    Framework bootstrapping happens once per worker, not once per request.

  2. 02
    Waypoint

    PHP workers can handle many requests before being recycled.

  3. 03
    Waypoint

    RoadRunner can manage HTTP, jobs, gRPC, TCP, and other plugin-driven workloads.

  4. 04
    Migration check

    The app must be safe to run inside a long-lived process.

SEO Metadata

SEO Title Options

  1. PHP Microservices With RoadRunner: High-Performance Worker
  2. PHP Performance: Practical 2026 Guide
  3. Performance Playbook: PHP Performance

Meta Description Options

  1. Learn PHP Performance with a practical Performance framework, expert mistakes, implementation steps, examples, FAQ, and schema-ready guidance.
  2. Introduces RoadRunner as a Go-based PHP app server, covering worker loops, middleware, HTTP/2, and benchmarks vs php-fpm.

URL Slug

php-microservices-with-roadrunner-high-performance-worker-pattern-explained

Focus Keyword

PHP Performance

Additional LSI Keywords

  • Performance
  • PHP
  • RoadRunner
  • Microservices
  • Workers
  • PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained
  • production checklist
  • implementation guide
  • best practices
  • architecture decisions
  • testing strategy
  • performance impact

Table of Contents

Article overview

PHP Performance is the kind of topic that looks simple until it reaches production. Teams usually discover the real cost late: unclear boundaries, weak defaults, hidden maintenance work, and decisions that seemed harmless when the codebase was small.

The problem gets worse when the article, tutorial, or implementation guide only explains the happy path. This guide closes that gap with a practical framework, a comparison table, common mistakes, and a deep technical section you can use while planning real work.

Keep reading for the non-obvious part: the safest implementation is rarely the most impressive-looking one. It is the one your team can debug, test, document, and evolve without turning every future change into archaeology.

Key Takeaways

  • PHP Performance should be evaluated as a production decision, not only as a syntax or tooling choice.
  • The best implementation keeps responsibilities visible, with clear ownership, tests, documentation, and rollback paths.
  • Search visibility improves when practical depth, structured answers, and expert examples live on the same page.

[IMAGE: A mobile-first technical article layout showing the main concept, decision table, implementation checklist, and FAQ blocks. Alt: PHP Performance expert guide for Performance]

What PHP Performance means

PHP Performance means applying performance knowledge to a concrete engineering decision, then turning that decision into reliable code, documentation, and operational behavior. In practice, it combines the topic's core concepts with trade-off analysis, implementation boundaries, testing strategy, and maintenance discipline.

This is the definition worth optimizing for featured snippets because it avoids hype. It tells the reader what the topic does and what a professional implementation must include.

Why it matters now

The technical web is more crowded than it was a few years ago. Thin tutorials can still get indexed, but they rarely earn trust from senior developers, buyers, AI answer systems, or teams that need production guidance.

For performance topics, the strongest content now has three layers:

  • a clear answer for fast scanning
  • a practical framework for implementation
  • expert context that explains what breaks later

That same structure helps search engines understand the page. It also helps readers decide whether the advice fits their project.

Implementation framework

Use this framework before adopting the approach described in this article.

  1. Define the user problem and the production risk.
  2. Identify the smallest reliable implementation boundary.
  3. Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
  4. Add tests for the behavior that would hurt if it regressed.
  5. Document the trade-off, not only the final code.
  6. Measure the result with logs, metrics, or user-facing outcomes.
  7. Revisit the decision after real usage exposes edge cases.

The sequence is deliberately conservative. It keeps the work grounded in outcomes instead of novelty.

[IMAGE: A seven-step implementation framework with discovery, boundary design, configuration, tests, documentation, measurement, and iteration. Alt: PHP Performance implementation framework]

Practical comparison

Decision areaStrong approachWeak approachWhy it matters
ScopeSolve one clear problemMix unrelated concernsFocus improves testing and search intent
ArchitecturePut logic in explicit classes or documented boundariesHide behavior in templates or incidental callbacksFuture changes stay easier to review
Data flowPass prepared data into the view or endpointQuery or compute in presentation codeReduces regressions and performance surprises
TestingCover the risky behavior directlyTest only the happy pathCatches production failures earlier
DocumentationExplain trade-offs and limitsRepeat generic definitionsBuilds E-E-A-T and reader trust
OperationsTrack logs, metrics, and rollback stepsShip without measurementMakes the decision reversible

This table is intentionally practical. It gives a reviewer something to check before the implementation becomes expensive to change.

Expert workflow

Expert tip: "Treat PHP Performance as a system boundary. If the next developer cannot find where the decision lives, how it is tested, and when it should be avoided, the implementation is not finished."

A useful workflow is simple:

  • Start with the smallest working example.
  • Add the constraints that exist in your real project.
  • Remove anything that only demonstrates cleverness.
  • Write down the failure modes.
  • Add links to related decisions so future readers can navigate the topic cluster.

That last point matters for both humans and search systems. A single article can answer a question; a cluster proves authority.

Common mistakes

Mistake 1: Copying a pattern without its context

A pattern that works in a small demo can fail in a real application. The missing context is usually data volume, team experience, deployment process, security requirements, or observability.

Before copying the pattern, ask what assumption made it safe in the original example.

Mistake 2: Putting business logic in the wrong layer

This is the fastest way to make future debugging expensive. In Laravel, PHP, and server-rendered websites, presentation should receive prepared data, not discover rules on its own.

Keep decision logic in models, actions, services, policies, requests, jobs, or documented helpers where it can be tested directly.

Mistake 3: Optimizing for novelty instead of maintainability

Newer tools and language features can be valuable. They can also hide simple behavior behind unfamiliar syntax.

Use the option that makes the next production incident easier to understand.

Mistake 4: Publishing without a measurement plan

If the article describes a performance, SEO, security, or architecture improvement, define how success will be checked. Logs, tests, crawl diagnostics, analytics, and user behavior are all stronger than assumptions.

[IMAGE: A common-mistakes board with context loss, wrong layer, novelty bias, and missing measurement highlighted. Alt: PHP Performance common mistakes]

Image placeholders

  • [IMAGE: A concept diagram for PHP Performance with input, decision boundary, implementation, tests, and production feedback. Alt: PHP Performance concept diagram]
  • [IMAGE: A mobile screenshot-style checklist for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained. Alt: PHP Performance mobile checklist]
  • [IMAGE: A comparison table visualization for strong versus weak implementation choices. Alt: PHP Performance comparison table]

Video placeholder

[VIDEO: Insert a 5-8 minute YouTube walkthrough that demonstrates the main decision, the implementation boundary, the test strategy, and the production caveats for PHP Performance.]

Internal linking opportunities

Original Technical Deep Dive

The short version

RoadRunner is a PHP application server and process manager written in Go. Instead of starting a fresh PHP runtime for every request, it keeps PHP worker processes alive and sends requests to them.

That changes the performance profile:

  • Framework bootstrapping happens once per worker, not once per request.
  • PHP workers can handle many requests before being recycled.
  • RoadRunner can manage HTTP, jobs, gRPC, TCP, and other plugin-driven workloads.
  • The app must be safe to run inside a long-lived process.

The last point is the part people underestimate. RoadRunner is not "PHP-FPM but faster" as a drop-in mental model. It is a worker runtime. If your application leaks request state into static properties, singletons, global variables, open transactions, or mutable service objects, RoadRunner will expose it.

Use RoadRunner when you want low-latency PHP services, API workers, internal microservices, queue consumers, or Laravel/Symfony apps that are already disciplined about lifecycle boundaries. Do not use it to hide bad database queries, missing indexes, slow external APIs, or unbounded memory growth.

PHP-FPM vs RoadRunner

PHP-FPM uses a request-response process model:

Request
  |
  v
Nginx
  |
  v
PHP-FPM worker
  |
  +-- bootstrap framework
  +-- route request
  +-- run controller
  +-- return response
  +-- clean request memory

RoadRunner uses a persistent worker model:

Request
  |
  v
RoadRunner HTTP server
  |
  v
Already-running PHP worker
  |
  +-- receive PSR-7 request
  +-- run application
  +-- return PSR-7 response
  +-- wait for next request

The win is obvious: expensive boot work can be reused.

The cost is also obvious: memory and state live longer. The app is responsible for resetting anything that should not survive the request.

For small scripts, PHP-FPM is often simpler. For heavy framework APIs and service endpoints where boot time is meaningful, RoadRunner can be a strong fit.

Where RoadRunner fits in microservices

RoadRunner is useful when a PHP service should behave like a small standalone server:

  • JSON API microservices
  • internal service-to-service HTTP APIs
  • gRPC services
  • webhook processors
  • event consumers
  • queue workers
  • read-heavy service endpoints
  • sidecar services around existing PHP domain logic

It is not a reason to split a monolith by itself. Microservices add deployment, observability, data ownership, versioning, and incident-response costs. Use RoadRunner because a service boundary already makes sense, not because "microservices" sounds more serious.

A good RoadRunner service has:

  • A clear API contract.
  • A small owned database or explicit dependency boundary.
  • Health and readiness endpoints.
  • Structured logs with request IDs.
  • Timeouts on every external call.
  • Worker memory limits.
  • A repeatable benchmark against PHP-FPM or the old runtime.

[IMAGE: Supporting visual 1 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 1]

[IMAGE: Supporting visual 1 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 1]

Install the PHP worker packages

For a minimal PSR-7 HTTP worker:

composer require spiral/roadrunner-http nyholm/psr7

You also need the RoadRunner binary. In many projects, the binary is installed through the RoadRunner CLI package or downloaded as part of the deployment image. Keep the binary version pinned in production; do not pull "latest" during deploy.

Project layout:

service/
  app/
    Http/
      Kernel.php
  public/
  vendor/
  .rr.yaml
  worker.php
  composer.json

For a framework application, worker.php becomes the bridge between RoadRunner and your framework kernel.

Minimal RoadRunner configuration

Start with the smallest .rr.yaml you can reason about:

version: "3"

server:
  command: "php worker.php"
  relay: pipes

http:
  address: 0.0.0.0:8080
  middleware: ["headers", "gzip"]
  access_logs: true
  max_request_size: 32

  pool:
    num_workers: 4
    max_jobs: 1000
    allocate_timeout: 10s
    destroy_timeout: 10s
    supervisor:
      exec_ttl: 30s
      max_worker_memory: 128

logs:
  mode: production
  level: info

What this does:

SettingPractical meaning
server.commandStarts the PHP worker script.
server.relayUses standard pipes for communication between RoadRunner and PHP.
http.addressExposes the HTTP server on port 8080.
http.middlewareRuns RoadRunner HTTP middleware before/after PHP.
pool.num_workersStarts four PHP CLI worker processes.
pool.max_jobsRecycles workers after handling 1000 requests.
supervisor.exec_ttlKills work that exceeds the request time limit.
supervisor.max_worker_memoryRecycles workers when memory grows too far.

Use a small fixed worker count first. Dynamic scaling is useful later, but it makes early debugging harder.

The worker loop

The worker is the process RoadRunner keeps alive.

Minimal PSR-7 worker:

<?php

declare(strict_types=1);

use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;

require __DIR__ . '/vendor/autoload.php';

$factory = new Psr17Factory();
$worker = Worker::create();
$http = new PSR7Worker($worker, $factory, $factory, $factory);

while ($request = $http->waitRequest()) {
    try {
        $response = handle($request);

        $http->respond($response);
    } catch (Throwable $exception) {
        $http->getWorker()->error((string) $exception);
    }
}

function handle(ServerRequestInterface $request): ResponseInterface
{
    if ($request->getUri()->getPath() === '/health') {
        return new Response(200, ['Content-Type' => 'application/json'], '{"status":"ok"}');
    }

    return new Response(200, ['Content-Type' => 'application/json'], json_encode([
        'service' => 'orders-api',
        'path' => $request->getUri()->getPath(),
        'method' => $request->getMethod(),
    ], JSON_THROW_ON_ERROR));
}

This is the whole pattern:

  • RoadRunner starts worker.php.
  • The worker waits for a request.
  • The worker converts it into a PSR-7 request.
  • Your app creates a PSR-7 response.
  • The worker sends the response back to RoadRunner.
  • The same PHP process waits for the next request.

That loop is why the app has to be strict about request state.

Connecting a real application

Do not put business logic in worker.php. Treat it as the runtime adapter.

Better shape:

<?php

declare(strict_types=1);

namespace App\Http;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class Kernel
{
    public function __construct(
        private Router $router,
        private ErrorResponder $errors,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        try {
            return $this->router->dispatch($request);
        } catch (Throwable $exception) {
            return $this->errors->render($exception);
        }
    }

    public function terminate(ServerRequestInterface $request, ResponseInterface $response): void
    {
        // Flush per-request logs, metrics, spans, or scoped containers here.
    }
}

Then the worker loop stays small:

$kernel = require __DIR__ . '/bootstrap/app.php';

while ($request = $http->waitRequest()) {
    try {
        $response = $kernel->handle($request);

        $http->respond($response);
        $kernel->terminate($request, $response);
    } catch (Throwable $exception) {
        $http->getWorker()->error((string) $exception);
    }
}

If you use Laravel or Symfony, use their RoadRunner integration layer or a maintained bridge rather than hand-rolling the entire framework lifecycle. The hard part is not creating a response; the hard part is resetting the framework container correctly between requests.

Middleware: RoadRunner vs PHP middleware

There are two middleware layers.

[IMAGE: Supporting visual 2 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 2]

RoadRunner HTTP middleware runs in the Go server layer:

http:
  address: 0.0.0.0:8080
  middleware: ["headers", "gzip", "static"]
  headers:
    response:
      X-Service: "orders-api"
  static:
    dir: "public"
    forbid: [".php", ".env"]

Use this layer for infrastructure concerns:

  • gzip compression
  • static file serving
  • response headers
  • trusted proxy parsing
  • request size limits
  • metrics middleware
  • sendfile support

[IMAGE: Supporting visual 2 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 2]

PHP middleware runs inside your application:

final readonly class RequestIdMiddleware
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $requestId = $request->getHeaderLine('X-Request-Id') ?: bin2hex(random_bytes(16));

        $request = $request->withAttribute('request_id', $requestId);

        return $handler
            ->handle($request)
            ->withHeader('X-Request-Id', $requestId);
    }
}

Use this layer for application concerns:

  • authentication
  • authorization
  • tenant resolution
  • validation
  • domain-specific rate limits
  • request-scoped logging context
  • API version negotiation

Do not move business rules into RoadRunner middleware. Keep RoadRunner responsible for server behavior and PHP responsible for application behavior.

HTTP/2 and service APIs

RoadRunner's HTTP plugin can pass HTTP, HTTPS, FastCGI, HTTP/2, and HTTP/3 requests to PHP workers. That does not automatically make a bad API fast, but it gives you a more capable server layer than a minimal PHP-FPM setup.

For microservices, HTTP/2 is useful when:

  • internal clients reuse connections heavily;
  • one client sends many requests to the same service;
  • TLS termination and upstream configuration are controlled;
  • observability can distinguish transport issues from application latency.

Keep the first deployment boring:

http:
  address: 0.0.0.0:8080

Put RoadRunner behind a known reverse proxy or load balancer first. Add HTTP/2, TLS automation, mTLS, or H2C only after basic behavior, health checks, logging, and deploy rollback are reliable.

The long-lived worker problem

With PHP-FPM, the end of the request naturally clears most process memory. With RoadRunner, the process keeps running.

These are dangerous in long-lived workers:

final class CurrentUser
{
    public static ?User $user = null;
}
final class TenantContext
{
    private ?Tenant $tenant = null;

    public function set(Tenant $tenant): void
    {
        $this->tenant = $tenant;
    }
}
final class ReportController
{
    private array $rows = [];
}

Each of those can leak state between requests if not reset.

Prefer explicit request attributes, immutable DTOs, and scoped services that are recreated per request:

final readonly class AuthenticatedRequest
{
    public function __construct(
        public ServerRequestInterface $request,
        public User $user,
    ) {}
}

If you must keep mutable context, reset it in terminate():

final class RequestScope
{
    private array $values = [];

    public function put(string $key, mixed $value): void
    {
        $this->values[$key] = $value;
    }

    public function get(string $key): mixed
    {
        return $this->values[$key] ?? null;
    }

    public function reset(): void
    {
        $this->values = [];
    }
}

Worker loop:

while ($request = $http->waitRequest()) {
    try {
        $response = $kernel->handle($request);
        $http->respond($response);
    } finally {
        $requestScope->reset();
        gc_collect_cycles();
    }
}

Do not call garbage collection because it feels responsible. Measure memory growth first. Use worker recycling and memory limits as the safety rail, then fix the leak if memory climbs per request.

Database connections and transactions

Persistent workers also mean persistent database connection objects.

Rules:

  • Never leave a transaction open after a request.
  • Roll back on exceptions before responding with an error.
  • Reconnect when the database closes an idle connection.
  • Do not store tenant-specific connection state globally.
  • Do not let one request change SQL mode, timezone, search path, or role for the next request.

[IMAGE: Supporting visual 3 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 3]

Wrap request handling:

try {
    $response = $kernel->handle($request);

    $http->respond($response);
} catch (Throwable $exception) {
    $database->rollBackIfOpen();

    $http->getWorker()->error((string) $exception);
} finally {
    $database->disconnectIdleConnections();
    $tenantContext->forget();
}

This matters more in multi-tenant services where a stale tenant database connection can become a data isolation issue.

Worker pool sizing

Start with this mental model:

CPU-bound endpoint: worker count close to CPU cores
I/O-bound endpoint: worker count can be higher
memory-heavy endpoint: worker count limited by RAM

Example starting point:

http:
  pool:
    num_workers: 4
    max_jobs: 1000
    max_queue_size: 256
    allocate_timeout: 5s
    supervisor:
      exec_ttl: 30s
      max_worker_memory: 128

Then measure:

  • p50 and p95 response time
  • request queue wait time
  • worker memory over time
  • CPU saturation
  • error rate under load
  • database connection count
  • upstream timeout count

[IMAGE: Supporting visual 3 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 3]

Do not blindly set num_workers to 100. You can move the bottleneck from PHP boot time to database saturation, Redis connection pressure, or external API rate limits.

Benchmarks vs PHP-FPM

Benchmark the same application, same endpoint, same data, same server, same OPcache settings, and same reverse proxy path.

Test at least three endpoints:

EndpointWhat it reveals
/healthRuntime overhead and HTTP path cost.
/api/productsFramework, routing, serialization, and cache behavior.
/api/orders/{id}Real database, auth, and domain logic.

Run PHP-FPM:

wrk -t4 -c64 -d60s --latency http://127.0.0.1:8081/api/products

Run RoadRunner:

wrk -t4 -c64 -d60s --latency http://127.0.0.1:8082/api/products

Capture:

runtime      endpoint       req/sec   p50     p95     p99     errors
php-fpm      /health        ...       ...     ...     ...     ...
roadrunner   /health        ...       ...     ...     ...     ...
php-fpm      /api/products  ...       ...     ...     ...     ...
roadrunner   /api/products  ...       ...     ...     ...     ...

Interpret results carefully:

  • If /health improves but real endpoints do not, the bottleneck is not PHP startup.
  • If RoadRunner wins on p50 but loses on p99, inspect queueing, worker count, and long requests.
  • If both runtimes slow down at the same concurrency, inspect database and external calls.
  • If RoadRunner memory grows through the test, fix lifecycle leaks before shipping.
  • If PHP-FPM is already fast and simpler to operate, keep PHP-FPM.

RoadRunner tends to look strongest on boot-heavy framework requests where application initialization is a large part of total request time. It will not rescue a service that spends 90 percent of its time waiting on MySQL, Elasticsearch, S3, or another HTTP API.

Deployment shape

A simple container can run RoadRunner directly:

FROM php:8.2-cli

WORKDIR /app

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY . .

RUN composer install --no-dev --prefer-dist --optimize-autoloader

EXPOSE 8080

CMD ["./rr", "serve", "-c", ".rr.yaml"]

Production image notes:

  • Install required PHP extensions explicitly.
  • Copy a pinned RoadRunner binary.
  • Run as a non-root user where possible.
  • Send logs to stdout/stderr.
  • Add /health and /ready endpoints.
  • Stop accepting traffic before terminating workers.
  • Tune Kubernetes or process-manager timeouts around RoadRunner shutdown behavior.

For Kubernetes:

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 10

Readiness should fail when the app cannot accept new work. Liveness should fail only when the process is broken and should be restarted.

[IMAGE: Supporting visual 4 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 4]

Observability checklist

Add these before load testing:

  • request ID in every log line;
  • route name or endpoint label;
  • response status;
  • response time;
  • worker memory;
  • worker restart count;
  • request queue depth or saturation signal;
  • database query count and duration;
  • external HTTP call duration;
  • error class and service name;
  • deploy version.

RoadRunner can expose server-level metrics, but application metrics still matter. You need to know whether p95 latency came from worker allocation, PHP code, SQL, Redis, or a downstream service.

Failure modes to test

[IMAGE: Supporting visual 4 for PHP Microservices With RoadRunner: High-Performance Worker Pattern Explained, showing PHP Performance decisions, examples, and PHP, RoadRunner, Microservices. Alt: PHP Performance php-microservices-with-roadrunner-high-performance-worker-pattern-explained visual 4]

Before production, force these failures:

  • PHP worker throws an exception.
  • Worker exceeds memory limit.
  • Worker exceeds execution TTL.
  • Database connection drops after idle time.
  • External API times out.
  • Deployment sends SIGTERM during active requests.
  • One endpoint allocates a large array.
  • One request opens a transaction and fails.
  • One tenant request is followed by another tenant request.

If a service cannot recover from those cases in staging, RoadRunner will not make it more reliable in production.

When to stay with PHP-FPM

Keep PHP-FPM when:

  • The team understands it and the app is already fast enough.
  • The service is mostly I/O-bound and startup is not the bottleneck.
  • The codebase relies on request-end process cleanup.
  • You cannot test state reset properly.
  • Hosting does not allow long-running PHP processes.
  • You need the simplest possible operational model.

RoadRunner is a strong tool, not a mandatory upgrade.

The right question is not "is RoadRunner faster than PHP-FPM?" The right question is "does this service benefit enough from persistent workers to justify the lifecycle discipline?"

Production checklist

Before switching traffic:

  • Worker count is sized from load tests, not guesswork.
  • max_worker_memory and exec_ttl are set.
  • Every request-scoped service is reset.
  • Database transactions are cleaned up after exceptions.
  • Tenant context is cleared after every request.
  • Logs include request IDs and service version.
  • Health and readiness endpoints are separate.
  • Benchmarks cover real endpoints, not only /health.
  • Deploy and rollback behavior is tested.
  • PHP-FPM baseline is preserved until RoadRunner is proven.

Use RoadRunner when you want PHP to behave like a long-running service runtime. Treat it with the same seriousness you would give a Node, Go, or Java service: lifecycle, memory, metrics, shutdown, and backpressure are part of the application now.

FAQ

What is PHP Performance?

PHP Performance is a practical performance topic that should be evaluated through implementation scope, production risk, testing, documentation, and long-term maintainability.

When should a team use PHP Performance?

Use PHP Performance when it solves a real project constraint, improves clarity, or reduces operational risk. Avoid it when it only adds novelty or hides behavior from future maintainers.

What is the biggest risk with PHP Performance?

The biggest risk is copying a pattern without its context. Production systems need clear boundaries, rollback options, tests, and observability before a technique becomes dependable.

How do you test PHP Performance?

Test the smallest unit that owns the behavior, then add integration coverage for the path users or systems actually rely on. Include failure cases, configuration differences, and regression checks.

How does PHP Performance affect SEO and AI search visibility?

It improves visibility when the article gives a direct answer, expert context, structured headings, internal links, trustworthy references, and FAQ content that matches the visible page.

Conclusion

PHP Performance is worth doing when the implementation improves clarity, reliability, or delivery speed. It is not worth doing when it hides ownership, increases operational risk, or makes the system harder to explain.

Use the framework above as a review checklist. Then connect this topic to the rest of the project documentation so readers can move from concept to implementation without losing context.

Top