Concurrency, processes, and asynchronous I/O
Status: Complete. Last reviewed 2026-08-28.
Concurrency is about coordinating work whose lifetimes overlap. Parallelism is simultaneous execution. A PHP request can be single-threaded and still participate in races with other requests, workers, schedulers, retries, and external systems. The correctness boundary is shared state, not the number of threads in one handler.
Execution and isolation models
Section titled “Execution and isolation models”A process has its own virtual address space and operating-system resources. Threads within a process share memory and require synchronization around mutable state. Coroutines and fibers are cooperatively suspended computations: they preserve an execution stack but do not schedule themselves or make blocking operations non-blocking. An event loop waits for readiness and resumes callbacks or suspended tasks without dedicating a blocked thread to each operation.
PHP-FPM normally runs multiple worker processes. Each worker handles one request at a time, but workers overlap and share databases, caches, files, brokers, and remote APIs. Ordinary request-scoped PHP values disappear at request shutdown; process-level extension state and resources may persist. Queue workers and application servers live longer, so service instances, statics, connections, and captured tenant/request state may cross job or request boundaries.
Threads can provide in-process parallelism, while processes provide stronger memory isolation at higher communication and startup cost. Event-driven concurrency is particularly useful for many independent I/O waits. It does not speed CPU-bound PHP automatically: work that occupies the event-loop thread delays every other task unless it is divided, yielded cooperatively, or moved to another process/thread/service.
Blocking, non-blocking, and asynchronous are different
Section titled “Blocking, non-blocking, and asynchronous are different”A blocking call prevents its execution context from doing other work until completion. A non-blocking operation returns before it would wait, often requiring readiness polling. An asynchronous API represents completion later through a callback, promise/future, channel, or suspended task. The underlying operation may use non-blocking I/O, another thread, or another process; the API shape alone does not prove the mechanism.
Concurrency reduces elapsed time only when waits or independent work overlap and the constrained dependency has capacity. Starting 100 requests concurrently against a ten-connection pool does not create database capacity. It creates queueing, memory use, timeout interactions, and possibly a retry storm. Bound concurrency by the smallest relevant pool or downstream limit, and measure tail latency and error rate as well as throughput.
Fibers make suspension and resumption explicit but are a low-level primitive. A library or runtime must decide when to resume them, propagate failures, and integrate readiness, timers, and cancellation. Never assume calling a traditional blocking database or filesystem API inside a fiber lets other work progress.
Atomicity and race conditions
Section titled “Atomicity and race conditions”An operation is atomic only relative to a named observer and boundary. ++$value may be adequate for one execution context but does not make a database read-modify-write atomic. A race exists when correctness depends on an uncontrolled ordering:
- Request A reads balance 100.
- Request B reads balance 100.
- Both validate a withdrawal of 80.
- Both write balance 20.
The final value hides one withdrawal, while two external effects may have occurred. A transaction alone does not necessarily prevent this; the statements and isolation level must protect the invariant. Use a conditional atomic update, row lock, optimistic version check, unique constraint, serialized partition, or another mechanism owned by the shared system.
Mutexes protect memory only among participants using the same lock. A PHP process mutex cannot coordinate other hosts. A cache lease may expire while a slow holder still acts. A database constraint often protects durable invariants more directly than an application-distributed lock. Choose the primitive whose failure and ownership boundary matches the state.
Deadlock is cyclic waiting. Starvation means a participant repeatedly fails to obtain service even though progress occurs elsewhere. Livelock means participants react but collectively make no useful progress. Avoid broad or inconsistent lock acquisition, keep critical sections short, define ordering, and treat database deadlock errors as retryable only when the whole transaction can be safely replayed.
Cancellation, deadlines, and shutdown
Section titled “Cancellation, deadlines, and shutdown”A timeout limits how long one wait is allowed. A deadline is an absolute budget that can be propagated through nested operations. Cancellation communicates that a result is no longer wanted. These are related but not equivalent: returning a timeout response does not prove the database query, child process, remote request, or queued work stopped.
Cooperative cancellation requires code to observe a signal/token, stop launching work, release resources, and propagate cancellation to children. Shield only short cleanup or commit sections whose interruption would leave worse state. Distinguish cancellation from failure in logs and metrics; expected client abandonment should not page like an internal defect, but continuing expensive abandoned work may still be an operational problem.
Unix signals are process-level notifications. Signal handlers should set intent and let normal control flow perform cleanup; complex work inside a handler risks reentrancy and unsafe state. A graceful worker shutdown usually stops intake, allows bounded in-flight work to finish or return safely, checkpoints where appropriate, closes resources, and exits before an orchestrator’s hard-kill deadline. Define what happens to broker acknowledgements and external side effects if the deadline expires.
Backpressure and bounded work
Section titled “Backpressure and bounded work”Backpressure is how a slower consumer limits a faster producer. Without it, the buffer becomes the system: memory grows, queue age rises, stale work executes, and timeouts trigger retries that add more load. “Asynchronous” code that creates an unbounded promise, fiber, or job per item has only moved the wait into memory or a broker.
Useful controls include bounded queues, concurrency semaphores, broker prefetch limits, rate limits, admission control, batching, streaming, and rejecting or shedding low-value work. A bounded queue makes overload visible. Decide whether producers should block, receive an error, drop replaceable work, coalesce duplicates, or persist for later. The choice is a product correctness decision, not only infrastructure tuning.
Backpressure must cross boundaries. Limiting PHP tasks while dispatching unlimited jobs merely moves the backlog. A worker pool should align prefetch and concurrency with database connections, downstream quotas, CPU, and memory. Retry policies need exponential delay, jitter, attempt budgets, and a non-retryable classification so a failing dependency does not amplify traffic.
Structured concurrency and ownership
Section titled “Structured concurrency and ownership”Concurrent child operations should have a visible parent lifetime. The parent owns starting them, awaiting or cancelling them, collecting failures, and releasing resources. Detached work is difficult to observe and may outlive request authentication, tracing, or deploy boundaries. If work must outlive the request, promote it to a durable job with an explicit payload and idempotency policy rather than relying on a background callback.
When several child calls are required, define failure semantics: fail fast and cancel siblings, wait for all and return partial results, or tolerate named optional dependencies. Collect errors without losing the first cause. Preserve per-operation deadlines inside the overall request budget, and avoid retrying each layer independently.
Production diagnosis
Section titled “Production diagnosis”- Rising latency with low CPU: inspect connection pools, event-loop lag, lock waits, downstream latency, and worker/queue saturation.
- Throughput falls as concurrency rises: look for a saturated dependency, context-switch/allocation overhead, hot locks, and retry amplification.
- Memory climbs: count in-flight tasks, queued payloads, retained closures/fibers, response buffers, and long-lived service state.
- Duplicate effects: reconstruct commit, external call, acknowledgement, timeout, and retry order; do not label it “a queue bug.”
- Shutdown loses work: compare graceful timeout with maximum job duration and verify acknowledgement/checkpoint order.
- Intermittent tenant leakage: inspect singleton/static/captured state across long-lived request or job boundaries.
Use traces to see overlapping spans, metrics for in-flight work, queue age, pool utilization and event-loop delay, and logs with operation IDs and attempt numbers. Reproduce scheduling-sensitive defects with barriers or controlled interleavings rather than hoping repeated tests hit the race.
Current and legacy context
Section titled “Current and legacy context”- Current: PHP fibers provide cooperative suspension primitives; scheduling, I/O integration, cancellation, and policy come from the surrounding runtime or library.
- Common: FPM, queue workers, schedulers, and horizontally scaled processes create concurrency through shared infrastructure even when application code has no threads.
- Legacy:
pcntlprocess control and signal APIs remain useful in CLI workers but are platform/SAPI-sensitive. Long synchronous chains should not be relabelled asynchronous merely because work was placed on a queue.
Interview practice
Section titled “Interview practice”- BACKEND-CONCURRENCY-01 — Find concurrency in a single-threaded service
- BACKEND-CONCURRENCY-02 — Contrast processes, threads, event loops, and fibers
- BACKEND-CONCURRENCY-03 — Diagnose a read-modify-write race
- BACKEND-CONCURRENCY-04 — Bound an asynchronous fan-out
- BACKEND-CONCURRENCY-05 — Propagate cancellation and deadlines
- BACKEND-CONCURRENCY-06 — Design graceful worker shutdown