RoadRunner provides the ability to create custom plugins, event listeners, middlewares, etc., that extend its functionality. It uses the Endure container to manage dependencies. This approach is similar to the PHP container implementation with automatic method injection.
To create a custom plugin, you can follow these steps:
Define a struct with a public Init method that returns an error value.
Implement Serve and Stop only if the plugin starts a service.
Request dependencies using their respective interfaces and inject them using the Endure container.
Below you can find more information about the plugin interface, how to define a plugin, and how to access other plugins.
v6 migration
Use the module versions selected by the RoadRunner build. RoadRunner v3 uses v6 plugin modules and endure/v2. The Endure lifecycle and dependency injection interfaces stay the same. The RoadRunner library module is roadrunner/v3.
The API repositories now have separate roles. api contains protobuf source, not a Go module. api-go contains generated Go bindings. api-plugins contains Go plugin contracts, not RPC messages.
All import paths in this table start with github.com/roadrunner-server/:
Previous import
v6 plugin import
roadrunner/v2025/<package>
api/v4/build/<component>/v1
api/v4/build/lock/v1beta1
api/v4/build/status/v1beta1
api/v4/plugins/v1/{kv,lock,logger,status}
api-plugins/v6/{kv,lock,logger,status}
api/v4/plugins/v4/priority_queue
api-plugins/v6/priority_queue
Generated imports have no build/ segment. For example:
Update implementations, local interfaces, and call sites together:
Logging: use logger.Named from api-plugins/v6/logger or a local interface with NamedLogger(string) *slog.Logger. The old logger.Log interface is removed. Pool constructors, worker factories, and logger options also take *slog.Logger. Replace log.Info("started", zap.String("plugin", name)) with log.Info("started", "plugin", name). Slog has no Fatal, Panic, or DPanic methods.
Jobs: add a leading context.Context to DriverFromConfig and DriverFromPipeline. Calls become constructor.DriverFromConfig(ctx, key, queue, pipeline) and constructor.DriverFromPipeline(ctx, pipeline, queue). Existing Driver methods already take contexts. See the Jobs driver tutorial.
KV: every Storage method now takes a leading context, including Stop. Update calls such as storage.Get(ctx, key), storage.Set(ctx, items...), and storage.Stop(ctx). Construction becomes constructor.KvFromConfig(ctx, key). Pass the context to backend operations. See the KV contracts.
Queues: lock queue signatures use lock.Item and []lock.Item, not the old priority-queue package's named interface. Jobs defines its own jobs.Item. Both retain ID, GroupID, and Priority. Update queue type arguments and method signatures. The priority_queue package now declares the Go package name priorityqueue.
Pool defaults: direct calls to DynamicAllocationOpts.InitDefaults() must pass the base worker count: InitDefaults(cfg.NumWorkers). WorkerWatcher.Watch(workers) has no return value. Remove checks for its former error result. See the worker pool for execution and shutdown behavior.
Goridge frames: Header() and Payload() return frame-owned slices. Copy data that must survive frame reset or reuse. From, ReadFrame, and ReadHeader retain the supplied buffers; later writes can change those buffers. See the frame API.
Removed helpers: replace proxy.Cidrs from proxy_ip_parser with []*net.IPNet. Resetter no longer exposes Plugin.Reset(string); its resetter.Reset RPC remains available. OTEL removes HTTPHandler and TemporalHandler; use Plugin.Middleware and Plugin.WorkerInterceptor. Temporal no longer exposes ResetAP; normal activity-worker replacement is handled by the pool.
DTO compatibility
api-go/v6 uses the v1 message set. The lock/v1 package defines Request and Response. Regenerated PHP lock DTOs use RoadRunner\Lock\DTO\V1. The lock field numbers and types are unchanged; custom descriptor or protobuf Any users must account for the package-name change.
Relocation alone does not change the retained HTTP or Jobs wire fields and does not require a PHP worker-loop rewrite. RPC still uses Goridge and Go net/rpc, not Connect. See RPC compatibility for the MessagePack change.
Direct Centrifugo DTO users have separate changes. Use typed fields instead of Command.id/method/params and Reply.id/result, which are removed. The RateLimit RPC and its types are removed. UpdatePushStatusRequest.uid becomes analytics_uid at the same string field number 1; update generated accessors and JSON names. The proxy bindings add experimental NotifyCacheEmpty; a custom server must implement it or embed the generated unimplemented server. The RoadRunner plugin forwards this event to PHP; update handlers and DTOs as described in Centrifuge.
Interface
A plugin that starts a service implements Service, which provides Serve and Stop. Middleware and other plugins that do not start a service do not need those methods. Optional interfaces such as Named, Provider, Weighted, and Collector provide names, dependencies, initialization weights, and dependency collection.
Here is an example:
Plugin definition
To define a custom plugin, create a struct with a public Init method that returns an error value (you can use roadrunner-server/errors as the error package). In this method, you can access other plugins by requesting dependencies.
Disabling a plugin
Sometimes, you may want to disable a plugin at runtime based on certain conditions. For example, if there are no configurations for the plugin, or if there is an initialization error but you still do not want to stop server execution. In such cases, you can return the special type of error called Disabled, which can be found in the github.com/roadrunner-server/errors package. This type of error can only be used in the Init function of the plugin.
Dependencies
You can access other plugins by requesting dependencies in your Init method. All dependencies should be represented as interfaces, and a plugin implementing this interface should be registered in RR's container, Endure.
Configuration
In most cases, your services would require a set of configuration values. RoadRunner can automatically populate and validate your configuration structure using the config plugin via an interface.
YAML configuration sample
Plugin
Configuration type
Serving
Endure calls Serve() synchronously. Start blocking work in a goroutine owned by the plugin, then return an error channel promptly. A blocking Serve() prevents the remaining plugins from starting and prevents container shutdown.
Stop(ctx) must stop the service cooperatively and respect the context deadline. The endure.grace_period setting determines this deadline. Endure does not terminate plugin goroutines when the deadline expires.
Plugin
This example starts a local HTTP server. Its Stop method uses http.Server.Shutdown(ctx) to wait for active requests until the context expires.
http.ErrServerClosed is the normal result of HTTP shutdown, so the example does not send it to Endure. Other errors are sent through the buffered channel. Endure does not make plugin code thread-safe; the plugin must synchronize access to shared state.
Collecting dependencies at runtime
RoadRunner provides a way to collect dependencies at runtime via the Collects interface. This is very useful for middlewares or extending plugins with additional functionality without changing them.
Let's create an HTTP middleware:
Declare the required interface:
Implement the Collects interface in the plugin that accepts the middleware:
Important notes:
dep.Fits: method used to check all registered plugins that fit the specified interface.
func(pp any){}: is a callback. You can pass an existing method with a func (_ any) signature or anonymous as in the example.
(*Middleware)(nil): is the second argument of the dep.Fits method which should be an interface you want to find in the registered plugins.
RPC Methods
Expose an RPC receiver through RPC() any. Its exported methods use the Go net/rpc signature: an input argument, a reply pointer, and an error result. Do not add a context argument to these RPC methods when updating the Jobs or KV contracts.
Example based on the informer plugin:
Suppose we have created a file rpc.go. The next step is to create a structure:
Create the receiver type:
Add an exported method:
Add RPC() to the plugin:
RPC plugin will automatically find and register your RPC methods under your plugin name. So, for example, to call the Hello method you might use the following sample:
Tips
More about plugins can be found here: link