← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

PHP curl_multi

PHP PHP 7.0+ Advanced
debt(d7/e5/b3/t6)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The serial curl_exec loop pattern is regex-detectable by semgrep per detection_hints, but the subtle misuses (missing curl_multi_select, missing curl_multi_info_read, no concurrency cap) mostly surface via CPU profiling or code review rather than phpstan alerts.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says replace serial loops with a proper multi_exec/select loop or Guzzle Pool — that's more than a one-line swap; it restructures the HTTP-calling code and often affects error handling, but stays scoped to the HTTP client layer.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). curl_multi usage is confined to the HTTP-calling component; it doesn't shape the whole system, though long-running workers must be careful about handle cleanup which imposes some ongoing tax.

t6 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t6, between t5 and t7). Per misconception, devs don't even realize PHP supports concurrent HTTP in-process, and per common_mistakes the API has multiple non-obvious gotchas (busy-loop without select, silent per-handle failures via missing info_read, global vs per-handle timeout) that contradict how a simple curl_exec mental model works.

About DEBT scoring →

Also Known As

curl_multi_exec parallel curl concurrent HTTP PHP curl multi handle

TL;DR

curl_multi_* runs many HTTP requests in parallel from one PHP process, dramatically cutting total latency for I/O-bound work.

Explanation

The curl_multi_* family of functions lets a single PHP process execute many HTTP requests concurrently instead of serially. You create individual easy handles with curl_init(), attach them to a multi handle via curl_multi_add_handle(), then drive the batch with curl_multi_exec() and curl_multi_select(). Instead of waiting for each request to complete before starting the next, the sockets progress in parallel using the underlying libcurl event loop. For 20 API calls at 200 ms each, serial cURL takes ~4 s; curl_multi typically finishes in a bit over 200 ms.

This matters most for aggregation endpoints, fan-out queries to microservices, bulk webhook delivery, sitemap crawling, and any batch that hits external services. Without concurrency, PHP-FPM workers sit blocked on network I/O and the whole request budget is spent waiting. curl_multi keeps the CPU utilised and shrinks tail latency.

The API is lower-level than most PHP developers expect. You must loop while curl_multi_exec() returns CURLM_CALL_MULTI_PERFORM or reports active handles, calling curl_multi_select() to block until sockets are ready (avoid a busy-loop that burns 100% CPU). After the batch you collect responses with curl_multi_getcontent() and read per-handle info via curl_multi_info_read() to detect failures, then curl_multi_remove_handle() and curl_close() each easy handle to free resources.

Modern code usually reaches for Guzzle's Pool or Promise\Utils::settle, or Symfony HttpClient with a concurrent scheduler, which wrap curl_multi cleanly and add retries, redirects, and timeout handling. Even so, understanding the primitive helps debug high-concurrency clients, set sensible CURLMOPT_MAX_HOST_CONNECTIONS limits, and reason about connection reuse. Watch out for unbounded parallelism hammering downstream services, per-handle timeouts (CURLOPT_TIMEOUT), and DNS resolver limits on high fan-out.

Common Misconception

Developers assume PHP can only make one HTTP request at a time and reach for shell exec or process forking to parallelise. curl_multi has provided true concurrent HTTP inside a single PHP process since PHP 5, using libcurl's event loop under the hood.

Why It Matters

Serial HTTP in a request-scoped PHP process is often the dominant latency cost; curl_multi turns N * latency into roughly max(latency), which is the difference between a snappy dashboard and a timeout.

Common Mistakes

  • Busy-looping on curl_multi_exec() without curl_multi_select(), pegging a CPU core at 100%.
  • Forgetting to call curl_multi_remove_handle() and curl_close() on each handle, leaking sockets and memory across long-running workers.
  • Firing thousands of parallel requests with no concurrency cap, overwhelming downstream services or exhausting local file descriptors.
  • Skipping curl_multi_info_read() and assuming success — individual handle failures silently return empty bodies.
  • Setting a global timeout only, so one slow host stalls the whole batch instead of failing that handle.

Code Examples

✗ Vulnerable
<?php
// Serial: 20 requests * 200ms = ~4 seconds
$urls = [/* 20 URLs */];
$results = [];
foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $results[] = curl_exec($ch); // blocks on each request
    curl_close($ch);
}
✓ Fixed
<?php
// Parallel: 20 requests complete in ~max(latency)
$urls = [/* 20 URLs */];
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
    ]);
    curl_multi_add_handle($mh, $ch);
    $handles[$i] = $ch;
}

do {
    $status = curl_multi_exec($mh, $active);
    if ($active) {
        curl_multi_select($mh, 1.0); // block until sockets ready
    }
} while ($active && $status === CURLM_OK);

$results = [];
foreach ($handles as $i => $ch) {
    $results[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($mh, $ch);
    curl_close($ch);
}
curl_multi_close($mh);

Added 20 Jul 2026
Views 35
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 3 pings M 2 pings T 2 pings W 1 ping T 2 pings F 2 pings S 1 ping S 1 ping M 1 ping T 2 pings W 2 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 2 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Google 7 Bing 4 ChatGPT 2 PetalBot 2 SEMrush 2 Applebot 2 Meta AI 1 Ahrefs 1 Qwen 1
crawler 22
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
PHP php A server-side scripting language that generates web pages and APIs — the code runs on the server, and only its output (usually HTML or JSON) reaches the browser.

PHP is often the first server-side language people meet, and understanding its execution model — script starts fresh on every request, no memory between requests — explains most of how the web backend works: sessions, databases, and caching all exist to bridge that per-request amnesia.

💡 Start with PHP 8.x, declare(strict_types=1), and PDO — skip any tutorial that mentions mysql_query().

Ask Codex about PHP →
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Replace serial curl_exec() loops with curl_multi_add_handle() + a proper curl_multi_exec()/curl_multi_select() loop, or use Guzzle's Pool with a concurrency limit.
📦 Applies To
PHP 7.0+ web cli queue-worker guzzle symfony-http-client
🔗 Prerequisites
🔍 Detection Hints
foreach\s*\([^)]+\)\s*\{[^}]*curl_exec\s*\(
Auto-detectable: ✓ Yes phpstan semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant