---
title: FrankenPHP Extension Workers: Background PHP Thread Pools
description: Use FrankenPHP Extension Workers to run a dedicated PHP thread pool from a Go extension for queues, schedulers, event listeners, and custom protocols.
---
Extension Workers enable your FrankenPHP extension to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Useful for queue systems, event listeners, schedulers, etc.
If you don't need to make the worker configurable by the user (fixed script path, fixed number of threads), you can simply register the worker in the init() function.
// FrankenPHP extension worker static registration
package myextension
import (
"github.com/dunglas/frankenphp"
"github.com/dunglas/frankenphp/caddy"
)
// Global handle to communicate with the worker pool
var worker frankenphp.Workers
func init() {
// Register the worker when the module is loaded.
worker = caddy.RegisterWorkers(
"my-internal-worker", // Unique name
"worker.php", // Script path (relative to execution or absolute)
2, // Fixed Thread count
// Optional Lifecycle Hooks
frankenphp.WithWorkerOnServerStartup(func() {
// Global setup logic...
}),
)
}If you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their Caddyfile. This requires implementing the caddy.Provisioner interface and parsing the Caddyfile (see the frankenphp-queue Caddy module example).
If you are embedding FrankenPHP in a standard Go application without caddy, you can register extension workers using frankenphp.WithExtensionWorkers when initializing options.
Once the worker pool is active, you can dispatch tasks to it. This can be done inside native functions exported to PHP, or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or any other goroutine.
Use SendMessage to pass raw data directly to your worker script. This is ideal for queues or simple commands.
// FrankenPHP extension: dispatch raw messages to a worker via SendMessage
// #include <Zend/zend_types.h>
import "C"
import (
"context"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_queue_push(mixed $data): bool
func my_queue_push(data *C.zval) bool {
// 1. Ensure worker is ready
if worker == nil {
return false
}
// 2. Dispatch to the background worker
_, err := worker.SendMessage(
context.Background(), // Standard Go context
unsafe.Pointer(data), // Data to pass to the worker
nil, // Optional http.ResponseWriter
)
return err == nil
}Use SendRequest if your extension needs to invoke a PHP script that expects a standard web environment (populating $_SERVER, $_GET, etc.).
// FrankenPHP extension: invoke a worker PHP script via SendRequest (HTTP emulation)
// #include <Zend/zend_types.h>
import "C"
import (
"net/http"
"net/http/httptest"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function my_worker_http_request(string $path): string
func my_worker_http_request(path *C.zend_string) unsafe.Pointer {
// 1. Prepare the request and recorder
url := frankenphp.GoString(unsafe.Pointer(path))
req, _ := http.NewRequest("GET", url, http.NoBody)
rr := httptest.NewRecorder()
// 2. Dispatch to the worker
if err := worker.SendRequest(rr, req); err != nil {
return nil
}
// 3. Return the captured response
return frankenphp.PHPString(rr.Body.String(), false)
}The PHP worker script runs in a loop and can handle both raw messages and HTTP requests.
<?php
// FrankenPHP extension worker script: handles raw messages and HTTP requests
$handler = function ($payload = null) {
// Case 1: Message Mode
if ($payload !== null) {
return "Received payload: " . $payload;
}
// Case 2: HTTP Mode (standard PHP superglobals are populated)
echo "Hello from page: " . $_SERVER['REQUEST_URI'];
};
while (frankenphp_handle_request($handler)) {
gc_collect_cycles();
}FrankenPHP provides hooks to execute Go code at specific points in the lifecycle.
| Hook Type | Option Name | Signature | Context & Use Case |
|---|---|---|---|
| Server | WithWorkerOnServerStartup |
func() |
Global setup. Run Once. Example: Connect to NATS/Redis. |
| Server | WithWorkerOnServerShutdown |
func() |
Global cleanup. Run Once. Example: Close shared connections. |
| Thread | WithWorkerOnReady |
func(threadID int) |
Per-thread setup. Called when a thread starts. Receives the Thread ID. |
| Thread | WithWorkerOnShutdown |
func(threadID int) |
Per-thread cleanup. Receives the Thread ID. |
// FrankenPHP extension worker with lifecycle hooks
package myextension
import (
"fmt"
"github.com/dunglas/frankenphp"
frankenphpCaddy "github.com/dunglas/frankenphp/caddy"
)
func init() {
workerHandle = frankenphpCaddy.RegisterWorkers(
"my-worker", "worker.php", 2,
// Server Startup (Global)
frankenphp.WithWorkerOnServerStartup(func() {
fmt.Println("Extension: Server starting up...")
}),
// Thread Ready (Per Thread)
// Note: The function accepts an integer representing the Thread ID
frankenphp.WithWorkerOnReady(func(id int) {
fmt.Printf("Extension: Worker thread #%d is ready.\n", id)
}),
)
}