php runtime and server environment

Concurrency And Long-Running Process Caveats

Concurrency means more than one piece of work can be in progress at the same time. In PHP, that might be multiple PHP-FPM workers handling requests, multiple queue workers processing jobs, or a long-running runtime handling many requests in one process.

Long-running processes are useful, but they remove the comfort of a clean request ending quickly. You must think about duplicated work, shared state, memory, locks, stale connections, and graceful shutdown.

Duplicate work

If two workers can process the same logical action, the action should be idempotent or protected by a lock.

PHP example
<?php

declare(strict_types=1);

final class ProcessedJobs
{
    /** @var array<string, true> */
    private array $seen = [];

    public function markIfNew(string $jobId): bool
    {
        if (isset($this->seen[$jobId])) {
            return false;
        }

        $this->seen[$jobId] = true;

        return true;
    }
}

$processed = new ProcessedJobs();

foreach (['email-100', 'email-100'] as $jobId) {
    echo $jobId . ': ' . ($processed->markIfNew($jobId) ? 'process' : 'skip duplicate') . PHP_EOL;
}

// Prints:
// email-100: process
// email-100: skip duplicate

This in-memory example explains the idea. In real applications, idempotency is usually enforced with a database unique key, queue visibility rules, distributed lock, or an external idempotency key.

Shared state

Shared mutable state is dangerous when several requests or jobs can touch it. Files, caches, database rows, static properties, and external APIs can all become coordination points.

Use database transactions, unique constraints, atomic cache operations, queue semantics, or lock services when correctness depends on one worker winning. Avoid "check then write" logic that assumes no other worker can change the value between the check and the write.

Long-running process hygiene

Long-running PHP processes should:

  • clear per-job and per-request variables
  • avoid storing unbounded data in arrays or static properties
  • close or refresh stale database and HTTP connections
  • catch errors around each job so one failure does not kill the whole loop unexpectedly
  • periodically report memory and health metrics
  • stop gracefully when the process receives a shutdown signal
  • restart after a safe number of jobs or a memory threshold when appropriate
PHP example
<?php

declare(strict_types=1);

foreach (range(1, 3) as $jobNumber) {
    echo 'Processing job ' . $jobNumber . PHP_EOL;

    gc_collect_cycles();
}

echo 'Worker can shut down cleanly.' . PHP_EOL;

// Prints:
// Processing job 1
// Processing job 2
// Processing job 3
// Worker can shut down cleanly.

This is a toy loop, but the shape is common: process one unit of work, clean up, then continue or exit safely.

Deployments and old code

Long-running workers do not automatically use new code just because files changed on disk. A deploy should restart or reload workers so the new code, configuration, and dependencies are actually loaded.

This applies to queue workers, RoadRunner workers, Octane workers, FrankenPHP worker mode, Swoole/OpenSwoole servers, and custom daemons.

What you should be able to do

After this lesson, you should be able to explain why duplicate work happens, identify where idempotency or locking is needed, review long-running worker hygiene, and understand why deployments must restart persistent PHP processes.

Practice

Task: Review A Queue Worker

Write a small PHP example or checklist for a queue worker that may receive the same job twice.

Requirements

  • Show how the worker would skip a duplicate job.
  • Explain why an in-memory example is not enough for multiple real workers.
  • Include at least three long-running worker hygiene checks.
  • Include a deployment note about restarting workers after code changes.

Check your work

The answer should focus on correctness and operations, not just looping over an array.

Show solution
PHP example
<?php

declare(strict_types=1);

$seenJobIds = [];

foreach (['invoice-900', 'invoice-900', 'invoice-901'] as $jobId) {
    if (isset($seenJobIds[$jobId])) {
        echo $jobId . ': skip duplicate' . PHP_EOL;
        continue;
    }

    $seenJobIds[$jobId] = true;
    echo $jobId . ': process' . PHP_EOL;
}

// Prints:
// invoice-900: process
// invoice-900: skip duplicate
// invoice-901: process

In real workers, an in-memory array is not enough because each worker process has its own memory. Use a database unique constraint, idempotency table, queue visibility rules, atomic cache lock, or external lock service when duplicate processing would be harmful.

Worker hygiene checklist:

- clear per-job state after each job
- monitor memory and restart after a safe threshold
- refresh stale database or HTTP connections
- catch job-level errors so one bad job does not corrupt the loop
- handle shutdown signals so deploys can stop workers cleanly

After deployment, restart long-running workers. Otherwise they may keep executing old PHP code, old configuration, or old service container state.