Pogo
Overview
Pogo runs independent PHP jobs in FrankenPHP extension worker pools while the original request waits for their results.
It is useful for fan-out/fan-in work such as:
- independent API calls,
- independent computations,
- response fragments that can be built in parallel.
Pogo is not a queue. Jobs must complete within the request lifecycle.
Status and fit
Pogo is experimental. Use it for request-scoped parallelism where failures can safely fail the request or be retried by the caller.
Use it when:
- Work is independent and can run in parallel.
- Each job is expected to finish quickly enough for an HTTP request.
- You want separate worker pools for external APIs, CPU work, or critical paths.
Avoid it when:
- You need persistence, retry, delay, or cancellation.
- You need a long-running task runner.
- You need an event loop or fiber abstraction.
Build
Compile the module into FrankenPHP:
xcaddy build \
--with github.com/dunglas/frankenphp@v1.12.3 \
--with github.com/dunglas/frankenphp/caddy@v1.12.3 \
--with github.com/y-l-g/pogo/module@main
Install the PHP package:
composer require pogo/pogo
Caddy configuration
Configure Pogo as a Caddy global option. A default pool is required.
{
frankenphp
pogo {
pool default {
worker public/pogo-worker.php
num_threads 8
max_wait 30s
}
pool external_api {
worker public/pogo-worker.php
num_threads 16
max_wait 10s
}
pool cpu {
worker public/pogo-worker.php
num_threads 4
max_wait 60s
}
}
}
Pool directives:
| Directive | Required | Description |
|---|---|---|
worker | Yes | PHP worker script. |
num_threads | No | FrankenPHP worker thread count. |
max_wait | No | Maximum wait while sending a job to the worker pool. Defaults to 30s. |
Application integration
Create a job class that implements Pogo\JobInterface:
<?php
namespace App\Pogo;
use Pogo\JobInterface;
final class FetchPrice implements JobInterface
{
public function handle(array $args): mixed
{
return [
'sku' => $args['sku'],
'price' => 42,
];
}
}
Create public/pogo-worker.php by copying the package worker or by writing a custom worker that accepts the same payload and response envelope.
The default worker expects:
['class' => App\Pogo\FetchPrice::class, 'args' => ['sku' => 'A-100']]
and returns:
['ok' => true, 'result' => $value]
['ok' => false, 'error' => 'message']
Dispatch and await jobs inside a request:
$price = pogo_dispatch(App\Pogo\FetchPrice::class, ['sku' => $sku], 'external_api');
$stock = pogo_dispatch(App\Pogo\FetchStock::class, ['sku' => $sku], 'external_api');
$tax = pogo_dispatch(App\Pogo\CalculateTax::class, ['sku' => $sku], 'cpu');
return [
'price' => pogo_await($price, 2.0),
'stock' => pogo_await($stock, 2.0),
'tax' => pogo_await($tax, 2.0),
];
PHP API
pogo_dispatch(string $class, array $args = [], string $pool = 'default'): int;
pogo_await(int $handle, float $timeout = 5.0): mixed;
pogo_pool_size(string $pool = 'default'): int;
pogo_await() throws RuntimeException for invalid handles, timeouts, worker failures, and job exceptions.
Arguments and return values must be JSON-compatible. Resources, closures, cyclic data, and unserializable objects are unsupported.
Operations
- Keep Pogo jobs short enough for the calling request timeout.
- Use separate pools to isolate slow external APIs from CPU-heavy jobs.
- Size
num_threadsaccording to workload type and available CPU. - Treat job failures as request failures unless your application handles partial results.
Troubleshooting
- Invalid or unknown job class: confirm the class is autoloadable inside the worker.
- Job must implement
Pogo\JobInterface: update the class or use a custom worker. - Timeouts from
pogo_await(): increase the await timeout or reduce job runtime. - Pool missing: confirm the pool is defined in the
pogoCaddy block and the binary includes the module.