
Table of Contents
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.
spiral/roadrunner-grpc package, define your service contract in a .proto file, generate PHP stubs via protoc, and configure the grpc plugin in .rr.yaml to listen on port 9001 while pointing workers to your generated entrypoint.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.
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.
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.
| Criteria | REST + PHP-FPM | gRPC + RoadRunner |
|---|---|---|
| Inter-service latency (p99) | 15–50ms | 1–5ms |
| Payload efficiency | JSON text, verbose | Binary protobuf, 30–70% smaller |
| Contract enforcement | OpenAPI docs, runtime validation | Compile-time proto validation |
| Browser support | Native fetch/XHR | Requires grpc-web proxy |
| Debugging ease | cURL, browser devtools | grpcurl, specialized tooling |
| Worker memory footprint | Low (stateless) | Higher (persistent state) |
| Ecosystem maturity in PHP | Decade+ battle-tested | Growing, 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.
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.