Thursday, 10 September 2026

Long-Running PHP with EvolvePHP: Designing for Safe Reuse

Long-Running PHP with EvolvePHP: Designing for Safe Reuse

Long-running PHP is usually introduced as a performance story.

Boot the application once.

Keep the process alive.

Handle many requests or jobs.

Avoid repeating expensive initialization.

That can absolutely improve performance.

But while working on EvolvePHP 2, I have become more interested in a different problem:

How do you know the process is still safe to reuse?

That question changes how I think about persistent PHP.

Because keeping a process alive is easy.

Keeping it clean between executions is harder.

Traditional PHP gives you an automatic reset

With the classic request-per-process model, application state has a natural ending.

A request arrives.

PHP runs.

A response is produced.

The request ends.

Conceptually:

Start
  ↓
Boot
  ↓
Handle Request
  ↓
Response
  ↓
Process ends

Anything accidentally left in memory disappears with the process.

That is surprisingly useful.

Now imagine the process stays alive:

Boot
 ↓
Request A
 ↓
Request B
 ↓
Request C
 ↓
Request D
 ↓
...

Suddenly, state from Request A can potentially affect Request B.

The runtime has stopped giving us a clean slate automatically.

We have to create one ourselves.

The dangerous state is often ordinary state

Consider something simple:

final class TenantContext
{
    public static ?string $tenantId = null;
}

Request A sets:

tenantId = company-a

The request finishes.

Request B belongs to:

company-b

But if that static value was not reset correctly, we have a much more serious problem than a memory leak.

We have an isolation failure.

The same concern applies to:

  • authenticated users,

  • tenant context,

  • database transactions,

  • request caches,

  • listeners,

  • authorization state,

  • telemetry context,

  • temporary resources.

Long-running PHP turns lifecycle management into part of correctness.

EvolvePHP uses explicit execution boundaries

This is why EvolvePHP 2 does not model everything around an HTTP request.

The lower-level concept is an execution.

An execution might eventually represent:

HTTP request
Queue message
Scheduled job
CLI command
Worker task

The important part is that each unit of work gets a clear lifetime.

The model I am using is broadly:

Application
    │
    ├── Execution A
    │
    ├── Execution B
    │
    └── Execution C

The application may survive.

The execution must not.

That distinction is fundamental.

Service lifetimes need to match reality

EvolvePHP currently distinguishes three service lifetimes:

Application
Execution
Transient

Application services may survive across many executions.

Execution services belong to one unit of work.

Transient services are created when requested and are not cached.

This allows the container to reason about lifetime relationships instead of treating every dependency as equivalent.

For example:

Application-scoped service
        ↓
Execution-scoped CurrentUser

is dangerous.

The application service could capture a user belonging to one execution and retain it into another.

That relationship should not quietly succeed.

This is one of the areas where dependency injection becomes more than convenience.

It becomes a safety boundary.

Cleanup needs to be deterministic

At the end of an execution, EvolvePHP closes the execution scope.

Services that explicitly participate in reset can be cleaned up in a deterministic order.

Conceptually:

Execution starts
      ↓
Services created
      ↓
Operation runs
      ↓
Reset participants
      ↓
Scope closes

The important word here is explicitly.

I do not think a framework should pretend it can magically identify every piece of state that needs resetting.

Services that own reusable state need a clear cleanup contract.

And cleanup needs to happen even when the operation itself fails.

What if cleanup fails?

This was one of the questions that influenced the runtime architecture heavily.

Suppose the business operation succeeds:

Payment processed successfully

but during cleanup:

TenantContext reset fails

What is the result?

The payment still succeeded.

We should not rewrite history and pretend that it failed.

But can we safely reuse the PHP process?

Probably not.

That is why EvolvePHP separates two questions:

Did the operation succeed?

Is the process safe to reuse?

They are not the same question.

An execution outcome can preserve the original result or exception separately from cleanup failure.

Then the runtime can make an explicit reuse decision.

Conceptually:

Handler succeeds
Cleanup succeeds
        ↓
      REUSE

but:

Handler succeeds
Cleanup fails
        ↓
    QUARANTINE

The process is treated as uncertain.

The safe response is not:

“Hopefully the next request is fine.”

It is:

Do not give this process more work.

Quarantine is deliberately fail-closed

I like this model because it avoids pretending we know more than we do.

If cleanup failed, the framework may not be able to prove exactly what state remains.

So rather than attempting clever recovery inside Core, the result says that process reuse is unsafe.

A runtime adapter can later decide whether that means:

stop accepting work
finish current response
restart worker
replace process

That operational policy belongs to the runtime.

The framework's job is to expose the truth.

This also changes error handling

A long-running runtime can have four interesting outcomes:

Operation success + cleanup success
Operation failure + cleanup success
Operation success + cleanup failure
Operation failure + cleanup failure

The first two may still leave the process reusable.

The last two should not.

That is quite different from saying:

exception = bad
no exception = good

Runtime safety requires more information than that.

EvolvePHP is not claiming complete persistent-runtime support yet

There is an important limitation here.

EvolvePHP 2 already has the execution-scope, cleanup and reuse/quarantine foundations.

But the broader persistent-runtime work is still ahead.

Concrete integration with runtimes such as FrankenPHP and later RoadRunner, stronger persistent-worker validation, concurrency abstractions and production runtime adapters belong to later development.

So I would not currently describe EvolvePHP as a finished persistent PHP platform.

The architecture is being prepared for that future.

That distinction matters.

Long-running PHP is not just about speed

I think this is the biggest lesson.

Persistent execution can make PHP faster.

But once the process survives the request, performance becomes only one part of the engineering problem.

You also need to think about:

Lifetime
Isolation
Cleanup
Ownership
Failure
Observability
Reuse

Because a process that handles 10,000 requests quickly is not impressive if request 9,427 sees state belonging to request 9,426.

The real goal is not:

Keep PHP alive as long as possible.

It is:

Keep PHP alive only while we still have evidence that the process is safe to reuse.

That is the direction I want EvolvePHP's long-running runtime model to take.

No comments:

Post a Comment