Async PHP: Making Your PHP Apps Wait Smarter, Not Harder

PHP has traditionally been viewed as a synchronous, short-lived scripting language - receive a request, block on I/O, render a response, and exit. That mental model worked perfectly when PHP mostly rendered HTML pages backed by a single database.

Today’s reality is different. PHP applications increasingly act as orchestrators - calling multiple APIs, talking to microservices, handling queues, WebSockets, and long-running workers. In these systems, waiting often dominates execution time.

Here’s the key insight: Async PHP is not about making PHP faster at computation. It’s about not wasting time while waiting.

In this article, we’ll explore what “async” actually means in PHP, why it matters for modern applications, how Fibers changed the language, and how frameworks like AMPHP make concurrency practical - complete with real code examples and real-world trade-offs.

 

Why Async Matters in Modern PHP

Let’s start with the fundamentals. PHP is single-threaded per execution context - there are no built-in threads for parallelism for the certain process. This architecture made sense for traditional PHP-FPM applications, but modern PHP backends are evolving:

  • Modern backends are no longer “render HTML and exit” - PHP applications now perform heavy I/O operations: API calls, database queries, queue processing and file operations
  • Long-running processes are becoming common - tools like Laravel Octane, worker processes and WebSocket servers keep PHP processes alive
  • Synchronous blocking wastes CPU time - applications spends significant time doing nothing while waiting for external responses

The problem isn’t speed - it’s waiting. And async PHP helps you overlap that waiting with useful work.

 

The Hidden Cost of Blocking I/O

To understand async, we first need to understand what makes blocking I/O problematic.

Blocking I/O means:

  • An I/O operation starts and the main process waits for a response
  • The CPU sits idle
  • No other work can proceed

Common I/O operations that block:

  • Network calls (HTTP requests, API calls)
  • Disk access (reading/writing files)
  • Database queries

In a single-threaded model, one blocked task means no progress on anything else. The problem isn’t that these operations are slow - it’s that while you’re waiting, you could be doing other productive work.

 

From Blocking to Concurrency

The solution is concurrency: structuring your program to make progress on multiple tasks without waiting for each one to finish sequentially.

Here’s how it works:

  • Overlap work - start multiple tasks without waiting for each to finish
  • Switch between tasks - let one task pause while another runs
  • Make progress elsewhere - keep the CPU busy on ready tasks instead of idling

This approach dramatically reduces total execution time for I/O-bound workloads.

 

Concurrency vs Parallelism: What’s the Difference?

These terms are often confused, but they represent different concepts:

 

Concurrency

  • Definition: Structuring a program to handle multiple tasks by making progress on more than one at a time
  • How it works: Task switching - only one task executes at any instant
  • Often described as: An illusion of parallelism
  • Examples: AMPHP, Node.js, Python asyncio

 

Parallelism

  • Definition: Executing multiple tasks simultaneously at the exact same point in time
  • How it works: Requires multiple CPU cores - tasks truly run in parallel
  • Limited by: Number of available CPU cores
  • Best for: CPU-bound workloads
  • Examples: Java threads, Go goroutines, PHP multiprocessing

Key principle: All parallelism is a form of concurrency, but not all concurrency is parallelism.

 

Real-Life Analogy

Think about preparing for a presentation while your car undergoes a technical inspection:

  • Concurrent approach: You drop off your car, work on your presentation while waiting, then pick up the car when ready. You’re switching between tasks, but only doing one thing at a time.
  • Parallel approach: You initialize an AI agent to prepare the presentation while you personally handle the car inspection. Both tasks happen simultaneously.

 

What Is Asynchronous Execution?

Asynchronous execution is a technique where a program initiates an operation (especially I/O) and continues doing other work instead of waiting for it to complete.

Key components of async systems:

  1. Non-blocking I/O - OS primitives that return immediately instead of blocking
  2. Event loop - Monitors registered I/O operations and determines when they’re ready
  3. Scheduler - Coordinates which tasks run and when, based on I/O readiness
  4. Cooperative multitasking (Coroutines) - Tasks voluntarily yield control so the scheduler can run other tasks

In simple terms: Async = structured waiting without blocking the main thread.

 

Cooperative Multitasking in PHP

PHP implements cooperative multitasking, also known as non-preemptive scheduling. In this model:

  • Tasks (coroutines) run until they voluntarily yield control
  • The scheduler does not interrupt them
  • Only one task executes at any instant, others are suspended

 

Before PHP 8.1: The Dark Ages

Before Fibers, PHP had no native mechanism to pause execution mid-stack. Async libraries relied on:

  • Callbacks (callback hell)
  • Generators (awkward syntax)
  • State machines (complex to maintain)

This made it harder to write linear, readable async logic.

 

After PHP 8.1: Fibers to the Rescue

A Fiber is a low-level language primitive introduced in PHP 8.1. It represents a stack-full, interruptible function - one that can be paused and resumed later.

Key characteristics:

  • Stack-full: Fibers have their own call stack, so they can suspend execution anywhere in nested function calls
  • Cooperative: A fiber only yields when it explicitly calls a suspension point (not preemptive)
  • Single execution: Only one fiber runs at any instant; others are suspended until resumed by the event loop

Fibers enable modern async frameworks to provide clean, readable async syntax that looks almost synchronous.

 

AMPHP: An Async PHP Framework

AMPHP is a collection of event-driven libraries for asynchronous programming in PHP. It allows you to write async code that looks synchronous.

Key features:

  • Readable async patterns - clean, linear code
  • Non-blocking I/O operations - non-blocking HTTP requests, database queries, file operations run
  • Built on Revolt Event Loop - manages I/O readiness and task scheduling
  • Cooperative scheduling - uses PHP Fibers orchestrated by the Revolt event loop

 

Amphp/amp Core Building Blocks

  1. Coroutines - Interruptible functions powered by PHP Fibers - Only one coroutine runs at a time - Suspension and resumption handled by the Revolt event loop
  2. Futures - Objects representing the eventual result of an async operation - Similar to promises in JavaScript - States: Pending, Completed, or Errored
  3. Combinators - Coordinate multiple futures concurrently - Functions like await(), awaitAll(), awaitAny()
  4. Cancellation - Request task cancellation gracefully - Does not forcefully kill the process
  5. Event Loop (Revolt) - Manages I/O readiness, timers, and task scheduling - The heart of async execution

 

Demo: Concurrent HTTP Requests with AMPHP

Let’s see async in action with a real-world scenario.

 

The Scenario

Imagine you need to fetch data from 5 different APIs:

  • Payment gateway (1s response time)
  • Weather service (8s response time)
  • User profile API (4s response time)
  • Analytics service (3s response time)
  • Notification system (2s response time)

Total wait time if sequential: 1 + 8 + 4 + 3 + 2 = 18 seconds

 

Solution 1: Sequential Requests (The Slow Way)

Here’s the traditional blocking approach:

<?php

require DIR . '/vendor/autoload.php';

use Amp\Http\Client\HttpClientBuilder;

use Amp\Http\Client\Request;

$httpClient = HttpClientBuilder::buildDefault();

$tasks = [

    ['name' => 'Task 1', 'duration' => 1],

    ['name' => 'Task 2', 'duration' => 8],

    ['name' => 'Task 3', 'duration' => 4],

    ['name' => 'Task 4', 'duration' => 3],

    ['name' => 'Task 5', 'duration' => 2],

];

$start = microtime(true);

try {

    foreach ($tasks as $task) {

        $uri = sprintf('https://httpbin.org/delay/%d', $task['duration']);

        echo "Starting: {$task['name']}\n";

        // This blocks until the request completes

        $response = $httpClient->request(new Request($uri, 'HEAD'));

        echo "✓ Completed {$task['name']}\n";

    }

} catch (Exception $e) {

    echo "Error: " . $e->getMessage() . "\n";

}

$end = microtime(true);

echo 'Total time: ' . round($end - $start, 1) . ' seconds' . PHP_EOL;

// Output: Total time: ~18 seconds

Problems with this approach:

  • Each request blocks the next one
  • Total time = sum of all response times (~18 seconds)
  • CPU is idle while waiting for network I/O
  • Poor user experience

 

Solution 2: Concurrent Requests with AMPHP (The Fast Way)

Now let’s use AMPHP’s async capabilities:

<?php

require DIR . '/vendor/autoload.php';

use Amp\Future;

use Amp\Http\Client\HttpClientBuilder;

use Amp\Http\Client\Request;

$httpClient = HttpClientBuilder::buildDefault();

$tasks = [

    ['name' => 'Task 1', 'duration' => 1],

    ['name' => 'Task 2', 'duration' => 8],

    ['name' => 'Task 3', 'duration' => 4],

    ['name' => 'Task 4', 'duration' => 3],

    ['name' => 'Task 5', 'duration' => 2],

];

$start = microtime(true);

try {

    // Launch all requests concurrently

    $requests = array_map(function ($task) use ($httpClient) {

        $uri = sprintf('https://httpbin.org/delay/%d', $task['duration']);

        // Amp\async() wraps the operation in a fiber

        return Amp\async(fn () => $httpClient->request(new Request($uri, 'HEAD')));

    }, $tasks);

    // Wait for all requests to complete

    $responses = Future\await($requests);

    foreach ($responses as $key => $response) {

        echo sprintf(

            "%s | HTTP/%s %d %s\n",

            $tasks[$key]['name'],

            $response->getProtocolVersion(),

            $response->getStatus(),

            $response->getReason()

        );

    }

} catch (Exception $e) {

    echo $e->getMessage() . "\n";

}

$end = microtime(true);

echo 'Total time: ' . round($end - $start, 1) . ' seconds' . PHP_EOL;

// Output: Total time: ~8 seconds

 

What Happens Under the Hood?

  1. Launch all 5 HTTP requests simultaneously - each wrapped in a fiber via Amp\async()
  2. Event loop monitors requests - tracks which operations are ready
  3. Requests complete independently - in overlapping time slots
  4. Total time = slowest response only (~8 seconds instead of ~18)

 

The Benefits

  • Same resources - no extra CPU needed
  • Use waiting time wisely - work on other tasks while the network responds
  • Better user experience - ~18s → ~8s (~10 seconds saved!)
  • Higher throughput - handle more requests with the same hardware

 

Real-World Async PHP Tools

Beyond AMPHP, several other tools and frameworks support async PHP:

Guzzle HTTP

  • Concurrent HTTP via promises
  • Uses I/O multiplexing (libcurl)
  • Not fiber-based; PHP execution remains synchronous under the hood

Swoole

  • PHP extension with a coroutine runtime
  • Event-driven server + worker processes
  • Enables async I/O and parallel task execution
  • High-performance for long-running servers

Workerman

  • Event-driven socket server in pure PHP
  • Long-running PHP processes
  • WebSocket and TCP servers with event loop

ReactPHP

  • Userland event loop for non-blocking I/O
  • Streams, sockets, timers
  • Node.js-style async programming in PHP

 

When to Use Async PHP

Async PHP shines in these scenarios:

I/O-bound workloads - API calls, database queries, file operations

Long-running PHP processes - workers, daemons, queue processors

Real-time services - chat servers, live notifications, presence systems

Long-lasting API calls - requests to AI models, third-party services

 

When to Avoid Async PHP

Async isn’t always the answer:

CPU-bound or disk-bound workloads - async won’t help with computational tasks

Simple applications - async complexity outweighs performance gains

Blocking dependencies - PDO, MySQLi, fopen(), fwrite() etc. block by design

Traditional PHP-FPM only environments - limited benefits without long-running processes

 

Important Distinction: FPM vs Long-Running Processes

In PHP-FPM, async optimizes a single request. The event loop restarts with each request, connections can’t be reused, and sockets don’t persist.

In long-running processes (Octane, workers, custom servers), async becomes an architectural foundation. You can:

  • Keep the event loop running
  • Reuse connections and sockets
  • Hold memory and state across requests
  • Build WebSocket servers, streaming responses, and real-time systems

The benefits of async grow with time and usage patterns.

 

Key Takeaways

  • Async is about I/O, not CPU - it helps you stop wasting time while waiting
  • Concurrency ≠ parallelism - concurrency is task switching; parallelism is simultaneous execution
  • Fibers changed everything - PHP 8.1 introduced stack-full, interruptible functions
  • AMPHP makes async practical - clean syntax, powerful abstractions, Fiber-based execution
  • Choose the right tool - async shines for I/O-bound, long-running workloads
  • Architecture matters - async benefits compound in long-lived processes

 

Sources and Further Reading