
August 14, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Implementing gRPC in PHP with RoadRunner getting started correctly solves the fundamental performance bottleneck of traditional PHP: the shared-nothing architecture that forces framework bootstrapping on every single request. While standard REST APIs over Nginx and PHP-FPM are perfectly adequate for public-facing web traffic, internal microservice communication demands the low-latency, strongly-typed contract that only gRPC provides via HTTP/2 and Protocol Buffers. RoadRunner bridges this gap by keeping your PHP application in memory as a persistent worker pool, eliminating bootstrap overhead and making high-throughput binary communication viable for production systems.
How do you configure RoadRunner for gRPC in PHP?
RoadRunner treats gRPC as a first-class plugin rather than an afterthought. Unlike running a standalone gRPC server in Go or Java, PHP requires an external application server to maintain state between requests. The configuration lives entirely in your .rr.yaml file at the project root. For teams familiar with building REST APIs in Laravel, this shift from routing files to declarative YAML configuration is significant but necessary for performance.
In my experience working on production systems where internal services communicate frequently, the default RoadRunner gRPC configuration rarely suffices. You must explicitly tune worker counts and timeouts to match your workload characteristics. A legal-tech portal I worked on required separate tuning for document validation services versus user authentication services because their computational profiles differed dramatically.
# .rr.yaml — Minimal production-ready gRPC configuration
version: "3"
server:
command: "php app.php"
relay: pipes
grpc:
listen: tcp://0.0.0.0:9001
proto:
- "proto/user.proto"
- "proto/document.proto"
max_send_msg_size: 50
max_recv_msg_size: 50
max_connection_idle: 0s
ping_time: 1m
timeout: 30s
pool:
num_workers: 4
max_jobs: 1000
allocate_timeout: 60s
destroy_timeout: 30s
logs:
level: warn
channels:
grpc: info
server: error The proto directive accepts multiple files or glob patterns. Each listed proto file gets compiled into PHP interfaces that your worker script must implement. The pool.num_workers setting controls how many PHP processes handle concurrent gRPC calls — start with CPU core count and adjust based on whether your service is CPU-bound or I/O-bound. Setting max_jobs: 1000 triggers automatic worker recycling after 1,000 requests, preventing memory leaks from accumulating in long-running processes. This is critical; I have seen services degrade after 48 hours when recycling was disabled.
Installing Required Dependencies
You need three components: the RoadRunner binary, the PHP protobuf extension, and the gRPC code generator. On Ubuntu 22.04 or 24.04 servers — which I use for most Nepal-based deployments — installation looks like this:
# Install protobuf PECL extension (required for message serialization)
sudo pecl install protobuf
echo "extension=protobuf.so" | sudo tee /etc/php/8.4/cli/conf.d/20-protobuf.ini
# Install RoadRunner via Composer (ensures version pinning)
composer require spiral/roadrunner-cli:^2026.0
./vendor/bin/rr get-binary --location ./bin
# Install protoc and PHP gRPC plugin
sudo apt-get install -y protobuf-compiler
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install github.com/roadrunner-server/protoc-gen-php-grpc@latest
# Verify installation
./bin/rr version
protoc --version Note that protoc-gen-php-grpc is specific to RoadRunner and differs from Google’s official protoc-gen-php. Using the wrong generator produces incompatible stubs. This mistake has wasted entire days on projects I have consulted on.
How do you define protobuf services and generate PHP stubs?
Protocol Buffers enforce a contract-first development workflow that prevents the drift common in JSON APIs. Every service method and message type must be explicitly defined before writing implementation code. For developers transitioning from REST API development, this feels restrictive initially but pays dividends in multi-team environments where interface stability matters more than iteration speed.
// proto/user.proto
syntax = "proto3";
package userservice;
option php_namespace = "App\\Proto\\UserService";
option php_metadata_namespace = "App\\Proto\\GPBMetadata";
service UserService {
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc CreateUser(CreateUserRequest) returns (UserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
message GetUserRequest {
string user_id = 1;
}
message UserResponse {
string user_id = 1;
string email = 2;
string full_name = 3;
int64 created_at_unix = 4;
}
message CreateUserRequest {
string email = 1;
string full_name = 2;
}
message ListUsersRequest {
int32 page = 1;
int32 per_page = 2;
}
message ListUsersResponse {
repeated UserResponse users = 1;
int32 total_count = 2;
} The php_namespace option controls where generated classes land in your PSR-4 autoloader structure. Always set this explicitly; relying on package name derivation creates fragile directory structures that break during refactoring. Generate stubs with:
# Generate PHP gRPC stubs compatible with RoadRunner
mkdir -p src/Proto/UserService
protoc \
--proto_path=proto \
--php_out=src/Proto \
--php-grpc_out=src/Proto \
proto/user.proto
# Regenerate composer autoload map
composer dump-autoload This produces two sets of files: message classes under App\Proto\UserService and a service interface UserServiceInterface that your worker must implement. Never edit generated files directly. If you need custom logic, extend the generated classes or wrap them in domain services.
How do you implement a gRPC service worker in PHP?
The worker script is the entry point RoadRunner executes and keeps alive. It boots once, loads dependencies, then enters an event loop accepting gRPC calls. This persistence model is fundamentally different from traditional PHP and requires careful attention to resource cleanup and state isolation.
<?php
// app.php — RoadRunner gRPC worker entry point
declare(strict_types=1);
use Spiral\RoadRunner\GRPC\Server;
use Spiral\RoadRunner\Worker;
use App\Proto\UserService\UserServiceInterface;
use App\Services\UserServiceHandler;
require __DIR__ . '/vendor/autoload.php';
// Bootstrap happens ONCE, not per request
$container = require __DIR__ . '/bootstrap/container.php';
$server = new Server(null, [
'debug' => false,
]);
// Register service implementation
$server->registerService(
UserServiceInterface::class,
$container->get(UserServiceHandler::class)
);
$worker = Worker::create();
$server->serve($worker); Your service handler implements the generated interface. Each method receives a typed request object and must return a typed response object. There is no middleware stack, no route resolution, no HTTP abstraction — just direct method invocation with binary serialization handled transparently.
<?php
// src/Services/UserServiceHandler.php
declare(strict_types=1);
namespace App\Services;
use App\Proto\UserService\UserServiceInterface;
use App\Proto\UserService\GetUserRequest;
use App\Proto\UserService\UserResponse;
use App\Proto\UserService\CreateUserRequest;
use App\Proto\UserService\ListUsersRequest;
use App\Proto\UserService\ListUsersResponse;
use App\Repositories\UserRepository;
use Spiral\RoadRunner\GRPC\Exception\GRPCException;
use Spiral\RoadRunner\GRPC\StatusCode;
final class UserServiceHandler implements UserServiceInterface
{
public function __construct(
private readonly UserRepository $users
) {}
public function GetUser(GetUserRequest $request): UserResponse
{
$user = $this->users->find($request->getUserId());
if ($user === null) {
throw new GRPCException(
'User not found',
StatusCode::NOT_FOUND
);
}
$response = new UserResponse();
$response->setUserId($user->id);
$response->setEmail($user->email);
$response->setFullName($user->full_name);
$response->setCreatedAtUnix($user->created_at->getTimestamp());
return $response;
}
public function CreateUser(CreateUserRequest $request): UserResponse
{
// Validation and creation logic
// Return populated UserResponse
}
public function ListUsers(ListUsersRequest $request): ListUsersResponse
{
// Pagination logic
// Return ListUsersResponse with repeated users
}
} A common mistake is treating these handlers like controller methods. They execute inside a persistent process. Database connections opened here stay open. Static variables retain values across calls. Global state accumulates. Always inject dependencies through constructors and avoid mutable static properties. For teams building modern Laravel architectures, adapting service container patterns to this constrained environment requires deliberate design.
When should you choose gRPC over REST for PHP microservices?
gRPC is not universally superior to REST. The decision depends on specific architectural constraints and team capabilities. After shipping both patterns across multiple client projects, I evaluate trade-offs along these dimensions:
| Criterion | gRPC + RoadRunner | REST + PHP-FPM |
|---|---|---|
| Latency (internal) | Sub-millisecond serialization, multiplexed streams | JSON encode/decode overhead, connection-per-request |
| Contract Safety | Compile-time type checking, versioned schemas | Runtime validation only, documentation drift |
| Developer Onboarding | Requires protobuf toolchain knowledge | Familiar HTTP verbs, curl-testable |
| Browser Compatibility | Requires grpc-web proxy layer | Native fetch/XMLHttpRequest support |
| Debugging | Binary payloads need specialized tools | Human-readable JSON, network tab inspection |
| Memory Footprint | Persistent workers, lower peak RSS | Per-process isolation, higher baseline memory |
| Ecosystem Maturity | Growing but smaller PHP community | Decades of libraries, middleware, hosting support |
Choose gRPC when internal services exchange high volumes of structured data with strict schema requirements. Choose REST when browser clients consume the API directly, when third-party integration is needed, or when team familiarity with protobuf is low. Many production systems I maintain use both: gRPC for backend-to-backend communication and REST for public endpoints. This hybrid approach captures performance benefits without sacrificing developer ergonomics where they matter most.
What are common pitfalls when deploying gRPC PHP services?
Production deployment of gRPC PHP services introduces failure modes absent in traditional PHP-FPM setups. Understanding these prevents costly debugging sessions after launch.
- Memory leaks in persistent workers: Every object allocated in a handler persists until garbage collected or the worker recycles. Circular references, unclosed database handles, and growing static caches accumulate silently. Always set
max_jobsin pool configuration and monitor RSS growth withrr workers. - Missing health checks: RoadRunner exposes HTTP health endpoints separately from gRPC ports. Load balancers probing the gRPC port directly cause connection errors. Configure health checks against the dedicated HTTP status port (default 2114).
- Protobuf version mismatches: Client and server must share compatible proto definitions. A field added server-side but missing in client-generated stubs causes silent data loss. Version proto files in git and regenerate stubs in CI pipelines, never manually.
- TLS termination complexity: RoadRunner supports TLS natively but certificate rotation requires process restarts. Most production deployments terminate TLS at a reverse proxy (Envoy, Nginx) and use plaintext internally. Document this boundary clearly.
- Cold start penalties: First request after deploy includes dependency injection container compilation and service registration. Pre-warm workers during deployment scripts by sending synthetic health-check-like requests before rotating traffic.
I encountered the memory leak issue on a document processing service where PDF generation libraries retained buffers across invocations. Workers grew from 80MB to 400MB over six hours before OOM kills triggered cascading failures. Adding max_jobs: 500 and explicit buffer cleanup resolved it permanently. These operational realities matter more than benchmark numbers when evaluating async computing futures for PHP.
Getting Production-Ready with gRPC in PHP
Successfully adopting gRPC in PHP with RoadRunner getting started requires treating it as infrastructure, not just another library. Define clear ownership for proto schemas, automate stub generation in CI, establish monitoring for worker memory and request latency, and document the operational runbook for on-call engineers. The performance gains are real — I have measured 3-5x throughput improvements over equivalent REST endpoints for internal service calls — but they come with operational complexity that demands respect. Start with a single non-critical service, validate your deployment pipeline end-to-end, and expand gradually. If your team lacks bandwidth for this operational overhead today, REST remains a perfectly valid choice; premature optimization toward gRPC creates technical debt faster than it creates value. When you are ready to implement or need guidance on whether gRPC fits your specific architecture, reach out to discuss your project requirements.

