Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

gRPC in PHP with roadrunner Getting Started

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.

gRPC ClientHTTP/2 + ProtobufRoadRunner ServergRPC PluginProcess SupervisorWorker Pool (N)PHP Worker 1Persistent StatePHP Worker 2Persistent StatePHP Worker NPersistent StateWorkers persist across requestsNo bootstrap per call
RoadRunner gRPC architecture: persistent PHP workers eliminate per-request bootstrap overhead for gRPC in PHP with RoadRunner getting started

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.

user.protoService DefinitionMessage Typesprotoc+ php-grpc pluginUserServiceInterface.phpMethod SignaturesType HintsUserResponse.phpMessage ClassGetters / SettersYour Implementationimplements InterfaceBusiness Logic
Protobuf compilation pipeline: proto definitions generate typed PHP interfaces consumed by your gRPC service implementation

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:

CriteriongRPC + RoadRunnerREST + PHP-FPM
Latency (internal)Sub-millisecond serialization, multiplexed streamsJSON encode/decode overhead, connection-per-request
Contract SafetyCompile-time type checking, versioned schemasRuntime validation only, documentation drift
Developer OnboardingRequires protobuf toolchain knowledgeFamiliar HTTP verbs, curl-testable
Browser CompatibilityRequires grpc-web proxy layerNative fetch/XMLHttpRequest support
DebuggingBinary payloads need specialized toolsHuman-readable JSON, network tab inspection
Memory FootprintPersistent workers, lower peak RSSPer-process isolation, higher baseline memory
Ecosystem MaturityGrowing but smaller PHP communityDecades 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.

New Service NeededBrowser Client Direct?YesNoUse RESTHigh Throughput / Low Latency?YesNoUse gRPCUse RESTTeam Knows Protobuf?YesNoUse gRPCStart RESTMigrate LaterHybrid architectures (gRPC internal + REST external) are common in production
Decision framework for choosing gRPC in PHP with RoadRunner getting started versus traditional REST based on real architectural constraints

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_jobs in pool configuration and monitor RSS growth with rr 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.

Frequently Asked Questions

It is a high-performance RPC framework using Protocol Buffers over HTTP/2, served by the RoadRunner Go binary instead of traditional PHP-FPM.

Zero licensing fees. Costs are developer time for protobuf definitions and server configuration, typically Rs 15,000–30,000 (~USD 110–220) for initial setup.

Use it for internal microservices requiring strict typing, low latency, and binary serialization. Stick to REST for public-facing client integrations or browser compatibility.

Yes, but not natively. You need the spiral/roadrunner-grpc package and must define services outside standard Laravel routing. In my experience integrating this on production Laravel systems, you treat gRPC workers as separate entry points that can still bootstrap the Laravel container for dependency injection and database access, though request handling differs significantly from HTTP controllers. Expect additional complexity managing two distinct service interfaces within one codebase.

RoadRunner 2024.x and later require PHP 8.2 minimum. I recommend PHP 8.3 or 8.4 for best protobuf extension compatibility and performance. Ensure your protobuf C extension matches your PHP minor version exactly. On Ubuntu servers running multiple PHP versions via ondrej PPA, verify the correct php-grpc extension loads in the specific CLI SAPI RoadRunner uses, not just FPM. Mismatched extensions cause silent worker crashes during startup that are difficult to diagnose without checking stderr logs carefully.

Install via PECL with pecl install grpc then enable extension=grpc.so in your CLI php.ini. On Ubuntu 22.04/24.04, you may need libgrpc-dev and php-dev packages first. For production deployments using Deployer, include this extension in your base server provisioning rather than installing during deploy. The compilation takes several minutes and requires build tools absent on minimal production images. Pre-built packages from ondrej/php PPA save significant deployment time and avoid compiler dependency issues across releases.

Significantly yes. RoadRunner keeps PHP workers resident in memory, eliminating bootstrap overhead per request. Each worker handles sequential requests while RoadRunner multiplexes across the pool. On a legal-tech portal I built, switching an internal document-validation service from FPM to RoadRunner reduced p99 latency from 180ms to 45ms under load. Configure worker count based on CPU cores, not RAM alone. Monitor worker restarts; frequent recycling indicates memory leaks in your protobuf message handling or uncaught exceptions terminating workers prematurely.

Write .proto files following proto3 syntax, then generate PHP classes using protoc with the grpc_php_plugin. Store generated code in a dedicated directory excluded from manual editing. Version your proto files alongside application code. In practice, I keep proto definitions in a shared repository when multiple services consume them. Regenerate after every schema change and commit the output. Missing regeneration causes runtime errors where method signatures mismatch. Use buf.build tooling for linting and breaking-change detection before deploying schema updates to production environments.

Uncaught exceptions, memory exhaustion, and protobuf deserialization failures top the list. Workers die silently unless you configure rr log level appropriately. Always wrap service method implementations in try-catch blocks returning proper gRPC status codes. Enable supervisor max_executions to recycle workers proactively before memory fragments. Check roadrr.log and stderr output when workers fail to start. On one eCommerce integration, malformed enum values from a legacy system crashed workers repeatedly until we added validation middleware before protobuf parsing occurred.

Pass credentials via metadata headers, similar to HTTP authorization. Implement a server interceptor that validates tokens before invoking service methods. JWT works well since gRPC lacks cookies. For internal services, mutual TLS provides transport-level security without token overhead. On client portals like Mijar Law Associates, we combine mTLS between services with JWT for user-context propagation. Never trust unvalidated metadata. Document your auth scheme in proto comments so consuming teams understand requirements without reading implementation code separately.

No. Browsers cannot speak native gRPC. Use grpc-web proxy or Connect protocol for browser clients. Alternatively, maintain a thin REST gateway translating HTTP to gRPC internally. For public-facing Nepal Gift Card APIs, we kept REST externally and used gRPC only between backend services. This avoids forcing frontend teams to adopt protobuf tooling while preserving internal performance benefits. If browser-to-gRPC is mandatory, evaluate Buf Connect which generates type-safe TypeScript clients compatible with RoadRunner backends without separate proxy infrastructure.

Enable Prometheus metrics endpoint in RoadRunner config and scrape with your existing monitoring stack. Track worker_active, worker_memory, and rpc_request_duration_seconds specifically. Set alerts on worker restart rate exceeding thresholds. On production deployments, I add custom histogram metrics inside service methods to capture business-logic latency separate from transport overhead. Combine with structured logging correlating request IDs across services. Without observability, gRPC debugging becomes guesswork since errors surface differently than HTTP status codes developers expect from traditional PHP applications.

RoadRunner runs as a systemd service, not under Apache or Nginx. Configure UFW to allow your gRPC port separately from HTTP ports. Ensure file ownership matches the systemd user running RoadRunner. On Ubuntu 24.04 servers, I create a dedicated roadrunner user with restricted permissions. Allocate sufficient open file descriptors via systemd LimitsNOFILE since each connection consumes one. Unlike PHP-FPM, there is no process manager inheritance. Misconfigured limits cause connection refusals under load that resemble network issues but are actually resource exhaustion at the OS level.

gRPC uses typed status codes instead of HTTP verbs. Return GRPC_STATUS_NOT_FOUND or GRPC_STATUS_INVALID_ARGUMENT explicitly rather than throwing generic exceptions. Clients receive structured error details via trailing metadata. Train your team to check status codes programmatically. On a booking system integration, downstream services initially returned generic INTERNAL errors for all failures, making debugging impossible. We refactored to return specific codes with localized messages in metadata. This contract-first discipline improves reliability but requires upfront design investment unfamiliar to many PHP developers accustomed to loose REST conventions.

Yes for internal service communication where you control both ends. For customer-facing endpoints serving diverse clients including mobile apps and third-party integrators, REST remains more practical given local developer familiarity. I have deployed RoadRunner gRPC successfully on legal-tech platforms processing sensitive documents between verification and storage services. Budget extra time for team onboarding and operational runbooks. The performance gains justify complexity at scale, but smaller projects benefit more from optimized Laravel REST APIs with proper caching than premature gRPC adoption adding maintenance burden without measurable user impact.

Share this article

Quick Contact Options
Choose how you want to connect me: