A bug that appears only after an application has been running for several hours can be surprisingly difficult to track down.
A request works perfectly, then another request behaves strangely. Memory usage keeps increasing. A background job uses data it should never have seen. Restarting the worker fixes the problem, but only temporarily.
Sometimes, the problem is not the business logic itself. It is how the application manages state between requests or jobs.
PHP's execution model matters
In a conventional PHP-FPM setup, PHP handles each request through a request lifecycle, even though the underlying FPM processes can serve many requests over time.
Long-running application servers work differently. Runtimes such as FrankenPHP worker mode and Laravel Octane can boot an application once and reuse it to handle subsequent requests. That reduces repeated startup work, but it also changes how developers must think about memory and state.
A static property, global variable, mutable singleton or in-memory cache may retain its value between requests when the runtime keeps that application state alive.
Imagine a service that stores the current user's ID in a static property. If the service is reused and the value is not reset, a later request could accidentally inherit the previous user's context.
That is not merely a performance issue. Depending on what is retained, it can become a correctness or security problem.
Shared state needs clear boundaries
Not all state is bad. Configuration and carefully managed caches can be useful to keep in memory. The problem is allowing data that belongs to one request or job to remain available to another.
A few habits help:
Keep request-specific information in request-scoped objects rather than mutable global state.
Be careful when registering mutable services as singletons or storing request objects inside long-lived services.
Reset custom service state between requests when the runtime does not do it automatically.
Release references to large objects and temporary data when they are no longer needed.
Monitor memory usage and configure worker recycling where appropriate.
Frameworks may reset some of their own state, but they cannot necessarily reset every custom object or global variable introduced by application code.
Worker recycling is useful as a safety measure, especially when dealing with libraries that retain memory. It is not a substitute for fixing a leak or resetting state correctly.
More workers introduce another problem
Keeping a worker alive is one thing. Running several workers at the same time is another.
Suppose an online store has only one item left. Two workers receive separate orders at almost the same time. Both check the inventory, both see one item available, and both proceed with the sale.
Adding workers has increased the application's capacity, but it has also exposed a race condition.
The database must enforce the actual business rule. Depending on the operation, that may mean using a transaction, an atomic update, a row lock or a unique constraint. Distributed locks can also help in appropriate situations, but they must be designed around failure and expiration.
The same principle applies to queue workers. A job may be retried after a timeout even if part of its previous attempt succeeded. Operations that must not happen twice need an appropriate idempotency strategy.
Concurrency is not just about making more work happen at once. It is about making sure that simultaneous work remains correct.
Give every worker a lifecycle
Long-running workers need more than a start command. They need a defined lifecycle.
That includes handling failures, logging enough context to diagnose them, cleaning up request-specific state, monitoring memory, and restarting workers safely during deployments. Queue workers also need sensible shutdown and retry behavior so that interrupted jobs do not leave the system in an inconsistent state.
The right approach depends on the runtime. A conventional PHP-FPM application does not automatically need to be redesigned around a long-running application server. Choose the execution model because it solves a real problem, then make its lifecycle part of the design.
A worker should be ready to handle the next request or job without carrying over information that belongs to the previous one. And when multiple workers operate on shared data, the application must protect its business rules against concurrent changes.
That is what makes long-running PHP workers reliable, not simply keeping them alive.
Further reading

No comments:
Post a Comment