DEV Community

Cover image for Building true distributed systems with RoadRunner and Laravel
Pavel Buchnev
Pavel Buchnev

Posted on

4 1

Building true distributed systems with RoadRunner and Laravel

I've been developing PHP applications for over 15 years now, working with various PHP frameworks. Throughout this journey, I've witnessed many projects that began as monoliths eventually need to evolve into microservices. That's when interesting architectural challenges emerge.

While I'd successfully used RoadRunner (RR) with Spiral and Symfony before, I wanted to bring these same capabilities to my Laravel projects. Yes, Laravel Octane does provide RR integration but only for HTTP. I needed the full ecosystem of plugins that make RR truly powerful for distributed systems.

If you're looking to build microservices where components communicate using gRPC, implement durable workflows with Temporal, or integrate your application with services written in other languages, you may have encountered certain limitations. Laravel excels at building robust applications, but when it comes to distributed systems with services written in multiple languages, additional tools can enhance its capabilities.

Laravel is an exceptional framework, and to extend its already impressive feature set, I wanted to create an integration with RR that goes beyond what Octane offers. This integration has transformed how I approach microservice architecture with Laravel, allowing me to build truly distributed systems while continuing to use the framework I often need for client projects. With dozens of services written in Go and Python, I've been able to seamlessly communicate with them from services written in Spiral and Symfony. Now, we can also use Laravel in this same ecosystem.

The challenge of building distributed systems

Before diving into solutions, let's understand the core challenges of distributed systems and microservices:

Laravel's queue system uses PHP's serialization under the hood, which means jobs serialized by Laravel are designed to be deserialized by PHP applications. This creates a consideration to address when building a microservice architecture with services written in different languages like Go, Python, or Node.js, as these services can't directly consume messages from Laravel queues without additional adapters.

Laravel's approach provides excellent developer convenience. It allows you to serialize virtually anything - objects, closures, functions, and complex structures - without having to think about format conversion. This is tremendously convenient when working within a PHP ecosystem, as you can queue complex objects with minimal effort. However, when communicating with non-PHP services in a distributed architecture, additional approaches can be beneficial.

Performance optimization opportunities

Traditional PHP implementations of queue systems often have certain characteristics to consider:

  1. Connection Management: PHP queue workers establish their own connection to the queue broker. The more workers you have, the more connections you need, increasing the load on your queue broker.
  2. Task Distribution: PHP workers independently poll the queue broker for tasks, essentially competing with each other. This leads to inefficient task distribution and wasted resources.
  3. Scaling Considerations: As you scale up PHP queue workers, the connection overhead and competition for tasks grows proportionally, limiting effective scaling.

RR takes a different approach to queue processing that can offer performance benefits:

  1. Centralized Connection Management: RR establishes a single connection to the queue broker.
  2. Intelligent Task Distribution: It fetches batches of tasks from the queue and efficiently distributes them among available PHP workers.
  3. Lightweight Communication: PHP workers communicate with RR using pipes rather than connecting directly to the queue broker, creating a more efficient system.

Let's dive into RoadRunner

At its core, RR is a complete application server written in Go. Where it truly shines is its plugin ecosystem that enhances PHP's capabilities while maintaining the development experience you already know and love.

RoadRunner plugins

Get started with RoadRunner by visiting the official RoadRunner documentation.

Let's explore some of the key plugins that make RR particularly valuable for developers:

🚀 HTTP Server

The HTTP plugin is usually where developers start with RoadRunner. It provides a high-performance server that can handle traditional HTTP connections, HTTPS with automatic TLS certificate management via Let's Encrypt, and HTTP/2 support.

What I really like about this plugin is that it totally removes the need for Nginx or Apache in the stack. I don’t have to mess with setting up Nginx or adding it to my docker-compose anymore — just one less thing to deal with.

When integrated with Laravel, this alone can provide significantly faster request handling compared to traditional servers. This performance boost comes from RR's core architecture: it creates long-running PHP workers that don't terminate after each request. Instead of bootstrapping your entire application for every request (loading the framework, config files, service providers, etc.), RR simply clears the application state and handles the next request with the already running process. This eliminates the substantial overhead of PHP's traditional request lifecycle.

http:
  address: 127.0.0.1:8080
  ssl:
    address: :443
    redirect: true
    acme:
      certs_dir: certs
      email: user@site.com
      domains:
        - site.com
Enter fullscreen mode Exit fullscreen mode

Read more about the HTTP plugin configuration in the official RoadRunner documentation.

📬 Jobs (Queues) plugin

RoadRunner Jobs plugin

It provides an efficient queue system where:

  • Queue brokers like RabbitMQ or Kafka handle the message transport
  • Messages can be consumed by services written in any language
  • All connections and heavy lifting are handled by Go, not PHP
  • You don't need to install and configure PHP extensions like php-amqp, or any other broker-specific extensions since all communication with queue brokers happens on the RR side
  • If you need to switch queue drivers (for example, RabbitMQ or Kafka), you only need to change RoadRunner's configuration. Your PHP code remains exactly the same, with no changes required. This level of abstraction provides flexibility and future-proofing for your application architecture.
jobs:
  consume: [ "emails", "default" ]
  pipelines:
    emails:
      driver: redis
      config:
        addrs: [ "redis:6379" ]
      priority: 10
      prefetch: 10
    default:
      driver: memory
Enter fullscreen mode Exit fullscreen mode

Read more about plugin capabilities in the RoadRunner Jobs documentation.

🌐 gRPC Support

In a distributed system, service communication is critical. REST APIs work well for many use cases, but gRPC offers some additional benefits for service-to-service communication:

  • A native Go implementation with excellent performance characteristics
  • Strong contracts through Protocol Buffer service definitions
  • Automatic code generation for type-safe client/server communication
  • Efficient communication through binary protocol
grpc:
  listen: "tcp://127.0.0.1:9001"
  proto:
    - "users.proto"
    - "orders.proto"
Enter fullscreen mode Exit fullscreen mode

This allows you to write your service definitions in proto files and implement the services directly in your application, with all the heavy lifting handled by Go. More importantly, other services written in different languages can communicate with your application using the same protocol and contract.

// users.proto
syntax = "proto3";

package users;

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

message GetUserRequest {
  string id = 1;
}

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

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}
Enter fullscreen mode Exit fullscreen mode

Read more about implementing gRPC services with RoadRunner in the gRPC plugin documentation.

⏱️ Temporal workflow engine

This is perhaps one of the most powerful yet less known plugins, and it's a game-changer for building distributed systems.

Temporal is an enterprise-grade workflow orchestration engine that addresses some of the complex challenges in distributed systems:

  • Durable Execution: Workflows can run for days, weeks, or even months with automatic resumability after failures
  • Distributed Transactions: Coordinate actions across multiple services without traditional 2-phase commits
  • Business Process Modeling: Express complex business processes directly in code
  • Durability: Workflows can survive application crashes, server reboots, and even network partitions, resuming exactly where they left off
  • Distributed Execution: Activities can be executed across multiple services and servers while maintaining workflow consistency
  • Saga Pattern Support: Built-in compensation mechanisms for distributed transactions that need to be rolled back
  • Sophisticated Retry Policies: Configurable retry strategies with exponential backoff
  • Cross-Language Support: Workflows and activities can be written in multiple languages (PHP, Go, Java, TypeScript), allowing different services to participate in the same workflow
  • Versioning: Support for workflow code versioning, ensuring that running workflows are not broken by code changes
temporal:
  address: 127.0.0.1:7233
  activities:
    num_workers: 10
Enter fullscreen mode Exit fullscreen mode

Here's a basic example of what a Temporal workflow might look like in your Laravel application. This workflow handles the entire lifecycle of a user subscription, from welcome email to trial period management to recurring monthly billing - all in one coherent piece of code:

#[\Temporal\Workflow\WorkflowInterface]
class SubscriptionWorkflow
{
    private $account;

    public function __construct()
    {
        $this->account = Workflow::newActivityStub(
            AccountActivityInterface::class,
            ActivityOptions::new()
                ->withScheduleToCloseTimeout(DateInterval::createFromDateString('2 seconds'))
        );
    }

    #[\Temporal\Workflow\WorkflowMethod]
    public function subscribe(string $userID)
    {
        yield $this->account->sendWelcomeEmail($userID);

        try {
            $trialPeriod = true;
            while (true) {
                // Lower period duration to observe workflow behavior
                yield Workflow::timer(DateInterval::createFromDateString('30 days'));

                if ($trialPeriod) {
                    yield $this->account->sendEndOfTrialEmail($userID);
                    $trialPeriod = false;
                    continue;
                }

                yield $this->account->chargeMonthlyFee($userID);
                yield $this->account->sendMonthlyChargeEmail($userID);
            }
        } catch (CanceledFailure $e) {
            yield Workflow::asyncDetached(
                function () use ($userID) {
                    yield $this->account->processSubscriptionCancellation($userID);
                    yield $this->account->sendSorryToSeeYouGoEmail($userID);
                }
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

More workflow examples you can find here.

Stop at this point and look at the code again. Notice what's missing? This workflow doesn't need to store state between executions in a database. There are no cron jobs checking for expired trials. All the logic is coupled in a single, easy-to-read class that serves as a clear starting point.

Now, think about how you would implement this same subscription process without Temporal:

You'd likely need:

  • A database table to track subscription states
  • A cron job running every day to check for trial expirations
  • Another cron job for monthly billing
  • Transaction handling to ensure billing and emails stay in sync
  • Error recovery code for failed charges
  • Complex restart logic if your server goes down during processing
  • Logging infrastructure to track the subscription lifecycle

With Temporal, all of this infrastructure complexity simply disappears. The workflow will survive server restarts, application deployments, and even service failures. The Temporal server manages all the complexity of distributed execution while allowing you to write straightforward, procedural-looking code.

The Temporal UI gives you a visual representation of your workflow's execution history:

Temporal observability

In this screenshot you can see the execution history of a subscription workflow over time. Each bar represents a 30-day cycle where the workflow was paused, and you can see events like "trialFinished" and "withdraw" (monthly billing) occurring exactly when scheduled.

Unlike traditional debugging where you're piecing together what happened from logs scattered across multiple systems, Temporal gives you a complete, visual timeline of your workflow's execution - even for processes that span months or years.

For PHP developers building distributed systems, this is powerful. It allows you to express complex multi-service workflows in simple PHP code while leveraging the full power of distributed systems architecture.

Learn more about Temporal in the official Temporal documentation and specifically for PHP in the Temporal PHP SDK documentation.

🔑 Key-Value (Cache broker)

The Key-Value plugin provides a PSR-16 compatible caching layer that works with Redis, Memcached, and in-memory storage. This gives you a high-performance caching solution managed by Go rather than PHP.

kv:
  in-memory:
    driver: memory
    config: {}
  users:
    driver: redis
    config:
      addrs:
        - "localhost:6379"
Enter fullscreen mode Exit fullscreen mode

📡 Centrifuge - Enterprise-grade real-time communication

For real-time applications, the Centrifuge plugin integrates with the Centrifugo websocket server, enabling scalable real-time communication between client and server.

This integration exemplifies RR's core philosophy: rather than reinventing the wheel, RR provides seamless integrations with battle-tested, production-ready third-party systems. Centrifugo is exactly such a solution - a high-performance, language-agnostic real-time messaging server with a large community and production deployments across various industries.

It gives you:

  • Scalable websocket connections (supporting millions of simultaneous connections)
  • Pub/sub messaging with flexible channel configurations
  • Client connection authentication and authorization
  • Automatic reconnection handling with message recovery
  • Cross-platform client libraries (JavaScript, iOS, Android, etc.)
  • Horizontal scaling capabilities for large deployments
centrifuge:
  proxy_address: "tcp://127.0.0.1:30000"
  grpc_api_address: tcp://127.0.0.1:30000
Enter fullscreen mode Exit fullscreen mode

Learn more about Centrifugo and its capabilities at the Centrifugo documentation and about implementing services with RoadRunner in the Centrifugo plugin documentation.

📈 Metrics (Prometheus) - Industry-standard observability

It allows you to monitor your application performance by collecting metrics that can be visualized in Grafana.

As with all RR plugins, it integrates with Prometheus - the de facto industry standard for metrics collection and monitoring in cloud-native applications. This integration gives you immediate access to a mature ecosystem of tools and practices that DevOps teams already know and trust.

  • Collect application-specific metrics (registered users, transactions, etc.)
  • Monitor system performance (memory usage, request latency, queue depths)
  • Create custom Grafana dashboards with real-time visualization
  • Set up alerts based on metric thresholds

This approach demonstrates RR's consistent philosophy: leverage proven, widely-adopted industry solutions. By integrating with the Prometheus ecosystem, RR ensures your Laravel application can be monitored using the same tools and practices employed by companies like Google, Digital Ocean, and Shopify.

metrics:
  address: localhost:2112
  collect:
    registered_users:
      type: counter
      help: "Total number of registered users."
    money_earned:
      type: gauge
      help: "The amount of earned money."
      labels: ["project"]
Enter fullscreen mode Exit fullscreen mode

Read more about metrics collection in the RoadRunner Metrics documentation.

🚢 Kubernetes (k8s) and container orchestration

RR is designed from the ground up to be Kubernetes-ready, with features specifically tailored for modern container orchestration:

Health Checks: RR's Status plugin provides a simple HTTP endpoint for monitoring the health of RR workers:

status:
  address: 127.0.0.1:2114
Enter fullscreen mode Exit fullscreen mode

These endpoints can be used directly as k8s liveness and readiness probes, allowing the orchestration platform to automatically restart unhealthy pods, ensure proper service discovery, and manage rolling updates safely. This built-in health monitoring brings enterprise-grade reliability to your PHP applications with minimal configuration.

Graceful Shutdown: During deployments and scaling events, RR implements graceful termination. When a pod receives a termination signal, RR won't immediately kill the process. Instead, it enters a grace period where:

  • It stops accepting new jobs/requests
  • It allows all current jobs and requests to complete processing
  • Only after all work is finished does it terminate the process

This prevents work from being abruptly interrupted during deployments, avoiding data loss or inconsistent state. For long-running jobs, this behavior is particularly valuable as it ensures that critical tasks are completed even during infrastructure changes.

Memory Management: RR includes sophisticated memory monitoring and management capabilities. You can configure memory limits per worker, and if a worker exceeds its allocated memory threshold, RR will intelligently restart that worker after a graceful shutdown period. This helps prevent memory leaks from impacting your entire application and maintains system stability without administrator intervention. For PHP applications, which can sometimes experience memory growth over time, this automatic worker recycling ensures optimal performance with minimal operational overhead.

These capabilities make RR an excellent choice for modern cloud-native applications, providing the reliability and operational features expected in production k8s environments without requiring additional sidecar containers or complex configuration.

Introducing the Laravel Bridge for RR

After working extensively with RR, I decided to build a comprehensive integration for Laravel that extends beyond what Octane offers. The result is the Laravel Bridge package, which exposes the full power of RR's plugin ecosystem.

The key difference is in the approach:

  1. Full Plugin Support: Bridge enables access to the complete RR ecosystem: Jobs, gRPC, Temporal, Key-Value, and more.
  2. Multiple Worker Types: It allows you to configure and run multiple workers for different RR plugins simultaneously.
  3. Octane Compatibility: It reuses Octane's SDK for clearing application state, ensuring compatibility with all Octane-compatible third-party packages.

Installation and basic setup

Getting started with the Bridge is straightforward:

composer require roadrunner-php/laravel-bridge
Enter fullscreen mode Exit fullscreen mode

Get started with the Bridge by visiting the GitHub repository.

Next, you'll need to create a .rr.yaml configuration file in your project root. This configuration file is the central control point where you enable or disable RR plugins.

Here's an example:

version: '2.7'

rpc:
  listen: tcp://127.0.0.1:6001

server:  
  command: 'php vendor/bin/rr-worker start'
  relay: pipes

http:
  address: 0.0.0.0:8080
  middleware: [ "static", "gzip" ]
  static:
    dir: "public"
    forbid: [ ".php", ".htaccess" ]

# Jobs plugin for distributed queues
jobs:
  consume: [ "notifications", "orders" ]
  pipelines:
    notifications:
      driver: memory
    orders:
      driver: kafka
      config:
        ...

# gRPC plugin for service communication
grpc:
  listen: tcp://0.0.0.0:9001
  proto:
    - "proto/users.proto"
    - "proto/orders.proto"

# Temporal plugin for workflow orchestration
temporal:
  address: temporal:7233
Enter fullscreen mode Exit fullscreen mode

Once you've installed the package and configured RR, you can start your application with:

./rr serve
Enter fullscreen mode Exit fullscreen mode

Learn more about configuration options in the RoadRunner Configuration documentation.

How It Works

RR creates a worker pool by executing the command specified in the server configuration:

server:  
  command: 'php vendor/bin/rr-worker start'
Enter fullscreen mode Exit fullscreen mode

Worker selection

When RR creates a worker pool for a specific plugin, it exposes the RR_MODE environment variable to indicate which plugin is being used. The Laravel Bridge checks this variable to determine which Worker class should handle the request based on your configuration.

The selected worker then listens for requests from the RR server and handles them using the Octane SDK, which clears the application state after each task (request, command, etc.). This ensures that your application remains in a clean state between requests, preventing memory leaks and state pollution.

Conclusion

What makes RR truly exceptional is that it lets PHP developers access first-class tools that were previously challenging to integrate: Temporal for durable workflows, Centrifugo for real-time communication, gRPC for efficient service communication, and many more. You get all of these capabilities while maintaining the Laravel development experience you're familiar with.

Image description

Whether you're scaling an existing application or building something new, RR gives you enterprise-grade tools with PHP-level accessibility. It's not about replacing Laravel—it's about supercharging it to thrive in today's complex application landscape.

DevCycle image

Ship Faster, Stay Flexible.

DevCycle is the first feature flag platform with OpenFeature built-in to every open source SDK, designed to help developers ship faster while avoiding vendor-lock in.

Start shipping

Top comments (1)

Collapse
 
nevodavid profile image
Nevo David

been wanting this kind of setup forever tbh, the tech mix makes things less crazy to manage. i gotta try this bridge soon

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

👋 Kindness is contagious

Discover more in this insightful article and become part of the thriving DEV Community. Developers at every level are welcome to share and enrich our collective expertise.

A simple “thank you” can brighten someone’s day. Please leave your appreciation in the comments!

On DEV, sharing skills lights our way and strengthens our connections. Loved the read? A quick note of thanks to the author makes a real difference.

Count me in