Open-source project
nunomaduro/pokio avatar
nunomaduro/pokio

pokio: promises for PHP, backed by forked processes

Pokio is a dead simple Asynchronous API for PHP that just works.

766 stars39 forksPHPMIT

At a glance

What is it?
pokio gives PHP a promise API with two global functions, async and await, and implements it by forking the process with PCNTL and passing data through a shared memory segment created with FFI. When those extensions are missing it falls back to sequential execution, and the README opens with a caution that the package is for internal use, not production.
Who is it for?
pokio fits a PHP developer working inside a toolchain that already forks, most obviously the Pest testing framework it was built to speed up, and who needs to run slow closures in parallel without pulling in an event loop extension. It does not fit a production service, because the README explicitly says not to use it there and gives no guarantees.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two functions and a promise

pokio is described as a dead simple asynchronous API for PHP that just works, and the example in the README is the whole pitch.

php
$promiseA = async(function () {
    sleep(2);
    
    return 'Task 1';
});

$promiseB = async(function () {
    sleep(2);
    
    return 'Task 2';
});

// just takes 2 seconds...
[$resA, $resB] = await([$promiseA, $promiseB]);

echo $resA; // Task 1
echo $resB; // Task 2

Two closures that each sleep for two seconds, awaited together, and the whole thing takes two seconds instead of four. The destructuring of the result array is a detail worth noticing, because it tells you that awaiting an array of promises returns an array of the resolved values in the same order.

The API surface is two global functions. The async function returns a promise that eventually resolves to the value returned by the given closure, and the await function blocks the current process until the given promise resolves.

php
$promise = async(function () {
    return 1 + 1;
});

var_dump(await($promise)); // int(2)

There is one more way to get a value out. Instead of using the await function you can invoke the promise directly, which returns the resolved value of the promise.

php
$promise = async(fn (): int => 1 + 2);

$result = $promise();

var_dump($result); // int(3)

That is a small API, and it is small on purpose, because the interesting part of the library is not the surface but what happens underneath.

The caution at the top of the README

The first thing the README says, in a blockquote right under the banner, is a caution, and it is unusually direct.

It says that this package manipulates process lifecycles using low-level and potentially unsafe techniques such as FFI for inter-process communication, and preserving state across process spawns. It is intended strictly for internal use, with the example given being performance optimizations in Pest. And it says do not use this in production or use it at your own risk, with no guarantees provided.

That warning is not boilerplate, and it is not the usual kind of hedging. Three specific things are named, and each one is a real source of trouble.

Process lifecycle manipulation means the library forks your process. A forked child inherits the parent's memory at the moment of the fork, and anything that changes after the fork is not in the child, and anything the child changes is not in the parent.

FFI for inter-process communication means the library reaches outside the language runtime. FFI is a low-level facility, and code that uses it can segfault, and it does so in a way that is very hard to debug because the fault is in a segment of memory you did not write.

Preserving state across process spawns means the library has to find a way to get a value from the child back to the parent, and whatever mechanism it uses has limits on what can cross.

The framing of the project as strictly internal is also a hint about the intent. The README names Pest as the motivating case, and that is a testing framework, where forking a process per test is an established pattern, where a crash in a child is contained, and where the whole run is short lived. The same code in a long running web worker is a different proposition entirely.

How the concurrency actually happens

The mechanism is described in two sentences, and they are the answer to how a language with no event loop gets concurrency.

Behind the scenes, pokio uses the PCNTL extension to fork the current process and run the given closure in a child process. That allows you to run multiple tasks concurrently without blocking the main process.

For communication between the parent and the child processes, pokio uses FFI to create a shared memory segment, which allows data to be shared between the processes fast and efficiently.

So there is no event loop, no fiber runtime, and no scheduler. Each async call is a fork, the closure runs in the child, and the result comes back through shared memory. The parent then waits for the children it was asked to await.

That has consequences that follow directly from the design. Two closures that sleep concurrently really do run at the same time, because they are two operating system processes. And the cost of each async call is a process spawn, which is far more expensive than a coroutine switch, which is why the library positions itself as an optimisation for closures that are genuinely slow rather than as a general purpose concurrency model.

The fallback behaviour is the other thing to understand. If PCNTL or FFI are not available, pokio automatically falls back to sequential execution, so you can still use it without any issues. Sequential execution means the promise still resolves to the right value and the await still returns it, but the two sleeping closures now take four seconds instead of two.

That fallback is what makes the library safe to install everywhere, and it is also the reason the behaviour of your code changes between environments. A test that is fast on a developer machine with the extensions and slow in a container without them is a confusing failure, and the README does not hide the mechanism behind which path was taken.

Chaining, catching, and promises that return promises

The promise API follows the conventions of other promise libraries, and the README documents the three chaining methods individually.

The then method is called when the promise resolves successfully, and it accepts a closure that receives the resolved value as its first argument.

php
$promise = async(fn (): int => 1 + 2)
    ->then(fn ($result): int => $result + 2)
    ->then(fn ($result): int => $result * 2);

$result = await($promise);
var_dump($result); // int(10)

The catch method is called if the given closure throws an exception, and it is optional, because you can use a native try and catch block around the await instead.

php
$promise = async(function () {
    throw new Exception('Error');
})->catch(function (Throwable $e) {
    return 'Rescued: ' . $e->getMessage();
});

var_dump(await($promise)); // string(16) "Rescued: Error"

The finally method is called regardless of whether the promise resolves successfully or throws.

php
$promise = async(function (): void {
    throw new RuntimeException('Exception 1');
})->finally(function () use (&$called): void {
    echo "Finally called\n";
});

And there is one rule that saves a lot of awkwardness. If you return a promise from the closure, it is awaited automatically.

php
$promise = async(function () {
    return async(function () {
        return 1 + 1;
    });
});

var_dump(await($promise)); // int(2)

That last one is what makes nested async work without a second await at each level, and it is a small touch that says the author thought about how the API would be used rather than only what the tests needed to cover.

The package, the tooling, and the release line

The project is on Packagist, and the install is one Composer command, with PHP 8.3 or newer required.

bash
composer require nunomaduro/pokio

The repository is a conventional PHP package with a composer.json, a src directory, a tests directory, a types directory, and a docs directory. The tooling is the modern PHP set. There is a phpunit configuration for the tests, a phpstan configuration for static analysis, a pint configuration for the code style, and a peck configuration, which is the static analysis tool that checks a package's public API surface.

The presence of a types directory is worth a moment, because the library is built around two global functions and the type declarations are what let an editor and an analyser know that an await of an array of promises returns an array. The library is MIT licensed, was created by Nuno Maduro, and the licence file and the security policy are both in the repository.

On the release history, the version numbers tell the story of a young package. v1.0.0 was published on 2026-04-04, v1.0.1 on 2026-04-12, and before those there was v0.1.2 from 2026-01-04. The last push to the repository was on 2026-10-14, several months after the 1.0 release, so the package has been stable and quiet rather than abandoned.

That combination, a one version past 1.0 with a caution at the top of the README, is the shape of a library that has found its use and is not trying to be anything more. The author points at Pest as the case it was built for, and the README's framing is consistent: an internal tool, documented carefully, with the sharp edges named rather than buried.

Editorial conclusion

pokio fits a PHP developer working inside a toolchain that already forks, most obviously the Pest testing framework it was built to speed up, and who needs to run slow closures in parallel without pulling in an event loop extension. It does not fit a production service, because the README explicitly says not to use it there and gives no guarantees. Verify first that your PHP build has the PCNTL and FFI extensions and understands what shared memory across forked children implies for your own process state, that the closures you pass do not depend on objects that cannot survive a process spawn, and that the sequential fallback is acceptable for the environments where you will actually run it.

Frequently asked questions

How does pokio run asynchronous tasks in PHP?

It uses the PCNTL extension to fork the current process and run the closure in a child process, so tasks run concurrently without blocking the main process, and it uses FFI to create a shared memory segment for passing data back to the parent.

Is pokio safe to use in production?

The README says not to. The package manipulates process lifecycles with low-level and potentially unsafe techniques such as FFI for inter-process communication and preserving state across process spawns, it is intended strictly for internal use such as performance optimisations in Pest, and no guarantees are provided.

What happens if PCNTL or FFI are not available?

Pokio falls back to sequential execution automatically, so the code still works and the promises still resolve to the right values, but the closures run one after another rather than concurrently, which means the runtime of your code changes between environments.

How do I install pokio?

With Composer, composer require nunomaduro/pokio, and PHP 8.3 or newer is required. The package is published on Packagist under the name nunomaduro/pokio.

How do you handle errors in a pokio promise?

You can chain a catch method, which is called if the closure throws, or you can use a native try and catch block around the await. A finally method is also available and is called whether the promise resolves or throws.

Official sources

  1. License: MIT
  2. nunomaduro/pokio on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nunomaduro-pokio.svg)](https://hysenlabs.com/projects/nunomaduro-pokio)

Was this useful?