Roadrunner runtime for Symfony

v2.2.1 2024-04-19 17:35 UTC

This package is auto-updated.

Last update: 2024-06-19 18:01:08 UTC


RoadRunner Runtime for Symfony

Yet another runtime for Symfony and RoadRunner.


composer require fluffydiscord/roadrunner-symfony-bundle


Define the environment variable APP_RUNTIME in .rr.yaml and set up rpc plugin:


        APP_RUNTIME: FluffyDiscord\RoadRunnerBundle\Runtime\Runtime

    listen: tcp://

Don't forget to add the RR_RPC to your .env:




  # Optional
  # Specify relative path from "kernel.project_dir" to your RoadRunner config file
  # if you want to run cache:warmup without having your RoadRunner running in background,
  # e.g. when building Docker images. Default is ".rr.yaml"
  rr_config_path: ".rr.yaml"
  # https://docs.roadrunner.dev/http/http
    # Optional
    # -----------
    # This decides when to boot the Symfony kernel.
    # false (default) - before first request (worker takes some time to be ready, but app has consistent response times)
    # true - once first request arrives (worker is ready immediately, but inconsistent response times due to kernel boot time spikes)
    # If you use large amount of workers, you might want to set this to true or else the RR boot up might
    # take a lot of time or just boot up using only a few "emergency" workers 
    # and then use dynamic worker scaling as described here https://docs.roadrunner.dev/php-worker/scaling
    lazy_boot: false

  # https://docs.roadrunner.dev/plugins/centrifuge
    # Optional
    # -----------
    # See http section
    lazy_boot: false

  # https://docs.roadrunner.dev/key-value/overview-kv
    # Optional
    # -----------
    # If true (default), bundle will automatically register all "kv" adapters in your .rr.yaml.
    # Registered services have alias "cache.adapter.rr_kv.NAME"
    auto_register: true

    # Optional
    # -----------
    # Which serializer should be used.
    # By default, "IgbinarySerializer" will be used if "igbinary" php extension 
    # is installed (recommended), otherwise "DefaultSerializer".
    # You are free to create your own serializer, it needs to implement
    # Spiral\RoadRunner\KeyValue\Serializer\SerializerInterface
    serializer: null

    # Optional
    # -----------
    # Specify relative path from "kernel.project_dir" to a keypair file 
    # for end-to-end encryption. "sodium" php extension is required. 
    # https://docs.roadrunner.dev/key-value/overview-kv#end-to-end-value-encryption
    keypair_path: bin/keypair.key

Running behind a load balancer or a proxy

If you want to use REMOTE_ADDR as trusted proxy, replace it with instead or else your trusted headers will not work.

Symfony is using the $_SERVER['REMOTE_ADDR'] to find out the proxy address, but in the context of RoadRunner, $_SERVER contains only environment variables and the REMOTE_ADDS is missing.

Response/file streaming

Build-in full support for Symfony's BinaryFileResponse and StreamedJsonResponse. The StreamedResponse needs one little change to be fully streamable - you have to change the callback to a \Generator, replacing all echo with yield. Look at the example:

use Symfony\Component\HttpFoundation\StreamedResponse;

class MyController
    public function myStreamResponse() 
        return new StreamedResponse(
            function () {
                // replace all 'echo' or any outputs with 'yield'
                // echo "data";
                yield "data";


Built in support for Sentry. Just install & configure it as you normally do.

composer require sentry/sentry-symfony

Centrifugo (websockets)

To enable Centrifugo you need to add roadrunner-php/centrifugo package.

composer require roadrunner-php/centrifugo

Bundle is using Symfony's Event dispatcher. You can create event listener for any event extending FluffyDiscord\RoadRunnerBundle\Event\Centrifugo\CentrifugoEventInterface:

  • ConnectEvent required :)
  • InvalidEvent
  • PublishEvent
  • RefreshEvent
  • RPCEvent
  • SubRefreshEvent
  • SubscribeEvent

Example usage:


namespace App\EventListener;

use App\Centrifuge\Event\ConnectEvent;
use RoadRunner\Centrifugo\Payload\ConnectResponse;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: ConnectEvent::class, method: "handleConnect")]
readonly class ChatListener
    public function handleConnect(ConnectEvent $event): void
        // original Centrifugo request passed from RoadRunner
        $request = $event->getRequest();
        // auth your user or whatever you want
        $authToken = $request->getData()["authToken"] ?? null;
        $user = ...

        // stop propagating to other listeners,
        // you have successfully connected your user

        // send response using the $event->setResponse($myResponse)
        $event->setResponse(new ConnectResponse(
            user: $user->getId(),
            data: [
                "messages" => ... // initial data client receives when connected

Be aware that if you do not set any response, bundle will send DisconnectResponse back by default.

Developing with Symfony and RoadRunner

  • If possible, stop using lazy loading in your services, inject services immediately.
  • It is no longer needed and might potentially bring issues to you like memory leaks.
  • Do not use/create local class/array caches in your services. Try to make them stateless or if they cannot be, add ResetInterface to clean up before each request.
  • Symfony forms might leak data across requests due to local caching it uses. Make sure your form defaultOptions are stateless. Do not store anything sensitive/important as it will be leaked in the following requests.
  • Simplify your User session serialization by taking advantage of EquatableInterface and custom de/serialization logic. This will prevent errors because of detached Doctrine entities and, as a side bonus, will speed up loading user from sessions.

namespace App\Entity\User;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
    private ?int $id = null;

    #[ORM\Column(type: Types::TEXT, unique: true)]
    private ?string $email = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $password = null;

    // serialize ony these three fields
    public function __serialize(): array
        return [
            "id"       => $this->id,
            "email"    => $this->email,
            "password" => $this->password,

    // unserialize ony these three fields
    public function __unserialize(array $data): void
        $this->id = $data["id"] ?? null;
        $this->email = $data["email"] ?? null;
        $this->password = $data["password"] ?? null;

    // check only the three serialized fields
    public function isEqualTo(mixed $user): bool
        if (!$user instanceof self) {
            return false;

        return $this->id === $user->getId()
            $this->password === $user->getPassword()
            $this->email === $user->getEmail()

Debugging (recommendations)

With RoadRunner you cannot simply dump and die, because nothing will be printed. I would like to introduce Buggregator to work around that. As a bonus it can also work as a mailtrap or testing Sentry locally


Inspiration taken from existing solutions like Baldinof's Bundle and Nyholm's Runtime