gRPC in PHP with roadrunner Getting Started

Khimananda Oli 8 min read Programming and Languages
gRPC in PHP with roadrunner Getting Started

By Khimananda Oli | Last reviewed: August 2026

Traditional PHP-FPM setups struggle with the latency overhead of REST when services communicate internally at scale. Implementing gRPC in PHP with RoadRunner getting started correctly replaces stateless request cycles with persistent binary workers, delivering sub-millisecond inter-service latency without abandoning the PHP ecosystem. This guide walks you through the exact production-grade configuration needed to run a compliant gRPC server that integrates cleanly with modern cloud-native infrastructure.

Why use RoadRunner for gRPC in PHP instead of FPM?

PHP-FPM is designed for HTTP: it boots, handles one request, tears down, and repeats. That model works fine for user-facing web traffic but collapses under internal service-to-service calls where payload size is small and frequency is extreme. Each FPM request pays the full bootstrap tax—autoloader, container compilation, config parsing—even if the actual business logic takes microseconds.

RoadRunner eliminates this by keeping PHP workers alive in memory. When combined with its native gRPC plugin, those workers speak HTTP/2 and Protocol Buffers directly without an Nginx or Envoy sidecar translating between protocols. The result is a true persistent process model where database connections, cache clients, and compiled protobuf descriptors survive across thousands of calls. For teams already running Laravel or Symfony, this means reusing existing domain logic inside a high-performance RPC boundary without rewriting in Go or Rust.

PHP-FPM ModelRequest 1BootstrapExecuteRequest 2BootstrapExecuteRoadRunner gRPCPersistentWorker(Stateful)Call ACall BHigh Latency OverheadNear-Zero Bootstrap Cost
PHP-FPM rebuilds state per request while RoadRunner gRPC workers persist connections and protobuf descriptors across calls

This architectural shift also aligns with observability best practices. Because RoadRunner exposes Prometheus metrics natively, you can track gRPC call counts, error rates, and worker memory without injecting middleware into every handler. If you are building towards SOC 2 compliance or need audit-ready infrastructure, having structured metrics emitted from the runtime itself simplifies evidence collection significantly compared to patching FPM logs.

How do you define protobuf contracts and generate PHP stubs?

gRPC relies on Protocol Buffers as both interface definition language and wire format. Before writing any PHP code, you must define your service contract in a .proto file. This single source of truth ensures type safety across polyglot teams and prevents the drift that plagues JSON-based APIs.

Create the proto definition

Create a proto/user.proto file in your project root. Keep messages flat initially; nested structures add serialization cost and complicate versioning.

syntax = "proto3";

package userservice;

option php_namespace = "App\\Grpc\\Users";
option php_metadata_namespace = "App\\Grpc\\Users\\Metadata";

service UserService {
  rpc GetUser (GetUserRequest) returns (UserResponse);
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
}

message GetUserRequest {
  string user_id = 1;
}

message CreateUserRequest {
  string email = 1;
  string display_name = 2;
}

message UserResponse {
  string user_id = 1;
  string email = 2;
  string display_name = 3;
  int64 created_at = 4;
}

Generate PHP classes with protoc

Install the protobuf compiler and the gRPC PHP plugin. On Ubuntu, this typically means installing protobuf-compiler and php-grpc via PECL or your system package manager. Then run generation:

protoc --proto_path=proto \
  --php_out=src/Grpc \
  --grpc_out=src/Grpc \
  --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin \
  proto/user.proto

This produces two sets of files: message classes under App\Grpc\Users and the base service stub under App\Grpc\Users\UserServiceInterface. Never edit these generated files. Treat them as build artifacts and regenerate on every CI run to prevent schema drift. For teams managing multiple services, consider reading about gRPC vs REST for service-to-service communication to validate that protobuf is the right contract boundary before committing to this workflow.

How do you configure RoadRunner to serve gRPC endpoints?

RoadRunner uses a declarative YAML configuration that separates transport concerns from application logic. The grpc plugin handles HTTP/2 framing, TLS termination, and worker dispatch independently of your PHP code.

Minimal .rr.yaml for gRPC

version: "3"

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

grpc:
  listen: tcp://0.0.0.0:9001
  proto:
    - "proto/user.proto"
  tls:
    key: "/etc/ssl/private/grpc.key"
    cert: "/etc/ssl/certs/grpc.crt"
  max_send_msg_size: 50
  max_recv_msg_size: 50

logs:
  level: warn
  encoding: json

Key details often missed in tutorials: the proto array tells RoadRunner which reflection descriptors to load for health checking and tooling compatibility. Without it, tools like grpcurl cannot discover your methods. The max_send_msg_size and max_recv_msg_size values are in megabytes; default limits of 4MB will silently drop larger payloads in production. Always set explicit bounds based on your actual payload profiles rather than relying on defaults.

For local development without TLS, omit the tls block entirely and connect clients with credentials: insecure. Never disable TLS verification in staging or production environments. If you're deploying to Kubernetes, pair this configuration with Kubernetes ingress and TLS with cert-manager to handle certificate rotation automatically at the edge while keeping internal mTLS optional during initial rollout.

gRPC ClientRoadRunnerPHP WorkerHTTP/2 + ProtobufDispatch to WorkerBinary ResponseHTTP/2 + Protobuf
Request flow from gRPC client through RoadRunner dispatcher to persistent PHP worker using binary protobuf over HTTP/2

What does a production-ready gRPC worker implementation look like?

The worker script is your application's entry point. It must bootstrap dependencies once, then enter RoadRunner's event loop. A common mistake is placing initialization logic inside the handler method—this defeats the entire purpose of persistent workers.

Implement the service handler

<?php

declare(strict_types=1);

namespace App\Grpc\Users;

use Spiral\RoadRunner\GRPC\ContextInterface;
use App\Domain\UserRepository;
use Ramsey\Uuid\Uuid;

final class UserServiceHandler implements UserServiceInterface
{
    public function __construct(
        private readonly UserRepository $users,
    ) {}

    public function GetUser(ContextInterface $ctx, GetUserRequest $in): UserResponse
    {
        $user = $this->users->findById($in->getUserId());
        
        if ($user === null) {
            throw new \Spiral\RoadRunner\GRPC\Exception\NotFoundException(
                'User not found'
            );
        }

        $out = new UserResponse();
        $out->setUserId($user->id);
        $out->setEmail($user->email);
        $out->setDisplayName($user->displayName);
        $out->setCreatedAt($user->createdAt->getTimestamp());
        
        return $out;
    }

    public function CreateUser(ContextInterface $ctx, CreateUserRequest $in): UserResponse
    {
        $user = $this->users->create(
            Uuid::uuid4()->toString(),
            $in->getEmail(),
            $in->getDisplayName()
        );

        $out = new UserResponse();
        $out->setUserId($user->id);
        $out->setEmail($user->email);
        $out->setDisplayName($user->displayName);
        $out->setCreatedAt($user->createdAt->getTimestamp());
        
        return $out;
    }
}

Wire the worker entrypoint

<?php

declare(strict_types=1);

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

use Spiral\RoadRunner\Worker;
use Spiral\Goridge\Relay;
use Spiral\RoadRunner\GRPC\Server;
use App\Grpc\Users\UserServiceHandler;
use App\Infrastructure\ContainerFactory;

// Bootstrap ONCE — DB pools, cache clients, config
$container = ContainerFactory::create();
$handler = $container->get(UserServiceHandler::class);

$server = new Server();
$server->registerService($handler);

$worker = Worker::create(Relay::pipes());
$server->serve($worker);

Note the explicit separation: container creation happens outside the serve loop. Database connection pools established here persist across all subsequent gRPC calls handled by this worker process. This is where RoadRunner delivers its performance advantage over FPM. Monitor worker memory consumption carefully; if your repository layer leaks references or accumulates state, workers will eventually exhaust memory and restart. Pair this with Prometheus metrics monitoring fundamentals to alert on worker restart frequency before it impacts latency percentiles.

How does gRPC with RoadRunner compare to traditional REST in PHP?

Choosing between gRPC and REST isn't purely technical—it depends on team maturity, client diversity, and operational constraints. The table below reflects real-world trade-offs observed across multiple production deployments in 2026.

CriteriaREST + PHP-FPMgRPC + RoadRunner
Inter-service latency (p99)15–50ms1–5ms
Payload efficiencyJSON text, verboseBinary protobuf, 30–70% smaller
Contract enforcementOpenAPI docs, runtime validationCompile-time proto validation
Browser supportNative fetch/XHRRequires grpc-web proxy
Debugging easecURL, browser devtoolsgrpcurl, specialized tooling
Worker memory footprintLow (stateless)Higher (persistent state)
Ecosystem maturity in PHPDecade+ battle-testedGrowing, fewer libraries

In practice, gRPC excels for internal service meshes where you control both ends of the connection and need predictable low-latency communication. REST remains superior for public APIs, mobile backends, and integrations with third-party systems that expect JSON over HTTP/1.1. Many teams successfully run both: RoadRunner serving gRPC internally behind an API gateway that translates to REST for external consumers.

New Service?Internal service-to-service?YesNo / PublicgRPC + RoadRunnerREST + PHP-FPM• Sub-ms latency• Strong typing• Persistent workers• Browser compatible• Simple debugging• Broad ecosystem
Decision framework for selecting gRPC RoadRunner versus REST FPM based on service boundaries and consumer types

Getting started with gRPC in PHP with RoadRunner today

Implementing gRPC in PHP with RoadRunner getting started requires disciplined setup but rewards you with performance characteristics previously unavailable in the PHP ecosystem. Begin with a single internal service, establish your protobuf conventions early, and instrument worker health metrics before scaling to additional endpoints. Avoid mixing HTTP and gRPC handlers in the same RoadRunner instance during initial adoption—isolate concerns until your team builds operational confidence.

If you're evaluating this architecture for a production migration or need help designing compliant infrastructure that passes security audits, reach out to discuss your specific deployment requirements. The transition from FPM to persistent workers touches networking, observability, and deployment pipelines simultaneously; getting the foundation right prevents costly rework later.

Frequently Asked Questions

PHP 8.4 or higher is recommended for full gRPC support and performance optimizations in RoadRunner 2026.x releases.

Run rr get-binary to download the server, then add spiral/roadrunner-grpc via Composer and enable the grpc section in your .rr.yaml configuration file.

Yes, you must install the protobuf PECL extension and generate PHP stubs using protoc-gen-php-grpc for type-safe message handling.

Yes, configure both http and grpc sections in .rr.yaml to serve REST and gRPC endpoints simultaneously from one binary process.

RoadRunner eliminates cold starts by keeping workers warm, typically delivering five to ten times lower latency than PHP-FPM for repeated gRPC calls.

Yes, specify cert and key paths under the grpc.tls config block to enable secure transport without an external reverse proxy.

Use protoc with the php-grpc plugin to output namespaced PHP message and service stubs compatible with RoadRunner’s runtime interface.

RoadRunner automatically restarts failed workers based on max_jobs and supervisor settings, ensuring continuous availability without manual intervention.

Yes, integrate via laravel-roadrunner packages, but note that Laravel’s container boots per-worker, so optimize service providers for long-lived processes.

Enable grpc.logging.level=debug in .rr.yaml and use grpcurl or BloomRPC to inspect request payloads, headers, and response codes locally.

Server-side and bidirectional streaming are supported as of RoadRunner 2026.1, requiring async-compatible PHP code and proper stream handler implementation.

Use client-side round-robin or L7 gRPC-aware proxies like Envoy, since RoadRunner workers handle concurrency internally within each node.

Typical baseline is 30–50 MB per worker depending on loaded services; monitor with rr workers command and adjust pool size accordingly.

Metadata keys must be lowercase ASCII; mixed-case keys are silently dropped due to HTTP/2 spec compliance enforced by the Go runtime layer.

Implement generated service interfaces in PHP classes registered via the grpc.services config array mapping proto package names to class paths.