Skip to main content

PHP Structured Concurrency and Beyond (4): call for backers

Merlin Rutz

What the series argued

Every step out of the stone age was the same trade: give up a freedom, get back a guarantee. goto went, then manual free(). Concurrency in PHP has not made that trade. We have fibers and an event loop, and nothing that owns them, and the engine is not about to change that. The guarantee the trade buys is local reasoning: you can tell what a function does by reading it. Optional structured concurrency, which is what the Scope RFC offered, pays the price and forgoes that gain.

A coroutine nobody owns is a leak. One request per process hid that, because exit collected everything; FrankenPHP, Swoole and RoadRunner take the hiding place away. A scope gives the work an owner. Tasks spawn inside it, it does not exit until the children are done, and an escaping exception tears down the siblings and arrives where the scope was opened. Read the function, and you know that when it returns, its work is done. Work that must outlive the request changes owner rather than going ownerless. Work started by foreign code is handled, and named as the accommodation it is.

And ownership says nothing about what the owned work can see. Every framework service holding per-request state, such as the current user or the negotiated language, bleeds between fibers. Drupal switched concurrency off where it happens; Symfony's answer is that every service should reset itself, forever. The third option is to leave the services alone and make the state resolve per task.

What exists today

Inventory as of 27 September 2026, so it stays readable when it is no longer true.

Real, tested, green on PHP 8.4: the task tree that records who started what, the task-local store that inherits along it, the Revolt driver that registers every fiber the loop creates so nothing is registered by hand, Scope on top of those, and the amphp interop including the unowned-await leak report. That is parts 1 and 2 of this series, and the proof points behind them.

Real and newer, therefore less proven: the structured-state layer, the delegator generation under it and the Symfony compiler pass that wires them, from part 3.

Begun: the Drupal adapter. Core creates its fibers inline, and the driver only sees fibers the loop creates, so the adapter runs only on a core that carries three small patches, one of them a fiber factory. On that core both traps are out and the regression tests for both filed bugs stay green. Nothing of it is on drupal.org yet, and nobody but me has reviewed it.

Not started: the Symfony bundle, and channels.

A family of packages, layered, each usable without the ones above it. Their boundaries and the API are what I am not publishing yet, and the closed repo is where they are discussed.

What is missing

The distance between "it works" and "you can build on it" is most of the work, and underselling it is how funded projects die at eighty per cent.

Edges. Every one found so far cost a day and changed a design decision: what a dropped scope does, why a destructor needs three references cut before it fires at the right moment, what an unowned wait means when the scope is already unwinding. Use finds more of them. Thinking does not.

amphp under real I/O. The tests run the interop against the loop, not against a slow socket, a server that half-answers, or a client library that swallows the cancellation token. That is where cooperative teardown either holds or fails.

The Drupal integration. An audit first: which core services hold per-request state and which are global. Then container wiring against a container that Drupal dumps and caches, so generated classes have to survive a rebuild. Then the Revolt driver in the kernel, clean state at the worker boundaries, and the fiber trap removed against tests that reproduce both filed bugs. And the core patches: core has to create its fibers through a factory before any of this can register them, and that lands on core's timeline, not mine.

Documentation, and a test suite that earns trust. Nobody puts an ownership layer under their request handling on the strength of a blog series.

Who this is for

Three communities, same bug, different evidence. If you are in one of them, this is what it buys you.

PHP at large. Fibers exist, nothing owns them, and cancellation is a token you thread by hand and hope every layer checks. The engine will not close this soon. The True Async RFC never passed a vote and was withdrawn in July. One of the two objections that sank it was global and static state, the problem the second half of this work solves. Whatever you build on fibers, you are building on a layer that has no ownership in it.

Symfony. Worker mode turns every stateful service into a latent bug, and the standing advice is that each service should reset itself. That is an audit that never ends, across code you do not own. api-platform is doing that audit (#7918); FrankenPHP gives the same advice (#207). Structured state is the alternative that does not require finishing it.

Drupal. Two state-bleed bugs, both answered by switching concurrency off (#3569172, #3576074). That works while core uses fibers in two places. It stops working when the renderer moves to Revolt (#3425212). The fiber trap then has to cover exactly the code paths that were supposed to get faster. The Drupal tier below is what retires it.

Within each community, the ones who pay for this are agencies, hosting and platform providers that run persistent processes at scale, and product companies on worker mode. For the hosters part 2 is the argument, not part 3. Institutions such as the Drupal Association and the PHP Foundation are welcome and run on a different clock; write anyway.

The ask

Two tiers, by audience. Each is a span of person-days on my own calibration, wide where a surprise would live.

The tiers are counted in days because that is how I calibrate work. What you buy is not the days. It is the fiber trap retired against your pages, or worker mode without the reset audit that never ends.

The package family. The userland libraries to production quality, tested, documented, open sourced. The edges use finds, the amphp interop under real I/O, the worker boundary on the runtime the first backer runs, the Symfony bundle, the review rounds, a test suite that earns trust, the docs, the release. Thirty to forty-seven person-days. This is the ask for the PHP and Symfony worlds, where the problem is worker mode and ownerless coroutines.

The package family plus Drupal integration. The module that tags and wraps Drupal's stateful services, so the fiber trap can be retired rather than extended. The scope is a service audit first, then container wiring, the Revolt driver in the kernel, clean state at the worker boundaries, the core patches that let core's fibers register, and the trap removed against regression tests that reproduce both filed bugs. Twenty-one to thirty-three person-days on top, plus standing core issue work that is calendar time rather than days.

The Drupal integration is an add-on, not a precondition. The package family stands without it, and the Drupal integration cannot be built without the packages.

The package family is the floor, counted in the money that funds days, not in licences. Below it nothing starts. The Drupal tier sits on it. Whatever comes in above both goes to the upper ends of the spans first, and then to channels.

Commitments, not payments, until 20 November. On that day either the floor is committed and the work starts in December, the API frozen in the first quarter of 2027, the family released in the course of the year and the Drupal tier after it. Or it is not, nothing is invoiced, and these four articles stand as what they are: a design, argued in public, that the world did not need badly enough. That is a legitimate outcome, and I will not argue with it.

Not part of the ask:

  • A core RFC. Userland is the deliverable, not a step towards internals.
  • Support, a hotline, an SLA. This funds development. If you want someone on retainer who knows where the state lives in your Drupal on FrankenPHP, that is a different agreement, and I am open to it. Write.
  • Contrib coverage beyond a backer's own set, or making Drupal core async.
  • A landing date for the core patches. That is core's to give.

What a backer gets

First, why this is not simply a public repo with a sponsor button. Open source used to pay for itself with the head start of having built the thing. In the age of language models a published design is reimplemented in days, and that head start is gone. What is left is the finished, tested thing and the person who knows why it is shaped that way. Whether that can still pay for the work, nobody knows, and I am not claiming to. This is one way to find out, and I would rather find out with you than alone.

What is built has a price, and it is the first thing a backer pays: forty to seventy person-days, in two implementations since March and the concept work before them. That is the licence. What a backer pays on top funds the days that remain, and only that part counts towards the floor. Both are due when the floor is committed, and not before.

A funded backer, in either tier, gets:

  • The repo and the issue queue now, under a one-page NDA, and a licence to run the code in production before it is public.
  • Their stack as the first proven target. Their services are audited as part of the work, not as an extra.
  • Scheduled review rounds. The API does not set until their objections are answered, in writing, in the issue. Not a vote: I decide, and every answer stays on record.
  • Credit in the packages and the announcement, unless they would rather not.

The stack row costs the days it costs, and comes with the larger shares; the licence alone buys the other rows. What "their stack" means depends on the tier. For the package family it is their runtime, FrankenPHP, Swoole or RoadRunner, and their services under the amphp interop with real I/O. For the Drupal tier it is their contrib set in the audit, the trap retired against their pages, and their bugs as regression tests.

Not in it: a vote, exclusivity, or a say on the licence.

WhoWhat
Funded backersrepo, issue queue, production licence, review rounds, credit
Everyone, lateropen source under EUPL-1.2

Qualified contributors get the first row without paying, by proposal.

Access comes step by step. While we talk, you read the docs, under the NDA, as a named user. When we close, you get the code. The articles are the public part, and they are enough to judge whether the conversation is worth having.

The last row is a commitment with a named condition: open source under EUPL-1.2 once the backers' concerns are worked in. Say it back to me if I miss it.

What comes after, if this finds backing

Two things, and only if it does. I am not promising work nobody is paying for.

The incubation model, as its own article beside this series. Why a design this size is developed in a closed round first, who decides, and why early public feedback on an unfinished design produces votes rather than review. It is contestable and it belongs off this series' critical path, so that disagreeing with how I publish does not mean dismissing what is published.

Channels. The next package after the family lands: directional ends, typed messages, discarding that propagates upstream, and channels that close with the scope that owns them. It is written up and waiting.

Who carries it

Who maintains these packages after release is open. Companies that run it, the upstream maintainers, core, the institutions: each can carry a part, and none has been asked. It is the conversation I want to have at both conferences.

Talk to me

I am at two conferences this autumn, and both beat an issue queue for this conversation.

DrupalCon Rotterdam, 28 September to 1 October, Postillion Hotel & Convention Centre. I am co-presenting Page Builder Deathmatch: Canvas vs Display Builder in a Real World Project with Andreas Rüther, Wednesday 30 September, 13:40. The topic is unrelated. Come for the page builders and find me afterwards about the fibers.

International PHP Conference, Munich, 26 to 30 October, Holiday Inn Munich City Centre. I am there on the 27th and 28th, attending, not speaking, which means I have time.

Otherwise, write to merlin [at] hook-dev-alter.com (merlin[at]hook-dev-alter[dot]com). Tell me which of the three communities you are in, what it costs you today, and what you would build if it did not. A "not for us, because" is as useful to me as a yes. It tells me where the argument stops.

"Las preguntas sirven para caminar." Questions are for walking. Subcomandante Marcos, La historia de las preguntas, 13 December 1994. The Zapatistas shortened it to preguntando caminamos: asking, we walk.

Disclaimer: my friend the babble-machine helped a lot with research, structure and wording. All errors are mine.

merlin portrait

Merlin Rutz

Based on 20 years in-depth drupal coding and a long-lasting contribution history Merlin helps teams to fix problems and introduce technical innovations. Founder and technical lead at HOOK_DEV_ALTER().

Alongside technical work, he brings experience with collaborative organizational models such as sociocracy, holacracy, and agile practices - helping teams improve how they share knowledge and make technical decisions.

merlinathook-dev-alter.com

Get notified about new articles.