Pogo
Overview
Pogo runs independent PHP tasks 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. Tasks 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 task 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.4 \
--with github.com/dunglas/frankenphp/caddy@v1.12.4 \
--with github.com/y-l-g/pogo/module@main
Pogo currently exposes native PHP functions directly. There is no required Composer package and no framework adapter in the core module.
Caddy configuration
Configure Pogo as a Caddy global option. The top-level worker config defines the required default pool. Named pool blocks add isolated pools.
{
frankenphp
pogo {
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 for the default pool and each named pool | PHP worker script. |
num_threads | No | FrankenPHP worker thread count. |
max_wait | No | Maximum wait while sending a task to the worker pool. Defaults to 30s. |
Application integration
Create a task class. It only needs to be autoloadable inside the worker and expose handle(array $args): mixed.
<?php
namespace App\Pogo;
final class FetchPrice
{
public function handle(array $args): mixed
{
return [
'sku' => $args['sku'],
'price' => 42,
];
}
}
Create public/pogo-worker.php by writing a worker that accepts the payload and returns the response envelope. The included async/example/worker.php is a small template.
The default worker expects:
['class' => App\Pogo\FetchPrice::class, 'args' => ['sku' => 'A-100']]
and returns:
['ok' => true, 'result' => $value]
['ok' => false, 'error' => 'message']
Spawn and await tasks inside a request:
$price = pogo_spawn(App\Pogo\FetchPrice::class, ['sku' => $sku], 'external_api');
$stock = pogo_spawn(App\Pogo\FetchStock::class, ['sku' => $sku], 'external_api');
$tax = pogo_spawn(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_spawn(string $class, array $args = [], string $pool = 'default'): int;
pogo_await(int $task, float $timeout = 5.0): mixed;
pogo_pool_size(string $pool = 'default'): int;
pogo_spawn() returns a task id. pogo_await() throws RuntimeException for unknown tasks, timeouts, worker failures, invalid worker responses, and task exceptions returned by the worker.
Arguments and return values must be JSON-compatible. Resources, closures, cyclic data, and unserializable objects are unsupported.
Operations
- Keep Pogo tasks short enough for the calling request timeout.
- Use separate pools to isolate slow external APIs from CPU-heavy tasks.
- Size
num_threadsaccording to workload type and available CPU. - Treat task failures as request failures unless your application handles partial results.
- Await every task you need. Unawaited tasks are canceled at request shutdown.
Troubleshooting
- Invalid or unknown task class: confirm the class is autoloadable inside the worker.
- Missing handle method: add a public
handle(array $args): mixedmethod or adapt your worker. - Timeouts from
pogo_await(): increase the await timeout or reduce task runtime. - Pool missing: confirm the pool is defined in the
pogoCaddy block and the binary includes the module.