This document covers important technical details about the async cURL integration, including known libcurl bugs and the workarounds applied.
The async cURL integration uses libcurl's multi_socket API combined with
the PAUSE/unpause pattern for non-blocking I/O:
- File uploads (
CURLFile):curl_mime_data_cb()with a read callback that returnsCURL_READFUNC_PAUSEwhile async file I/O is in progress. - File downloads (
CURLOPT_FILE): write callback returnsCURL_WRITEFUNC_PAUSEwhile async file write is in progress. - User callbacks (
CURLOPT_WRITEFUNCTION): write callback pauses, spawns a high-priority coroutine to run the PHP callback, then unpauses.
After async I/O completes, the transfer is unpaused via curl_easy_pause(CURLPAUSE_CONT)
and driven forward with curl_multi_socket_action(CURL_SOCKET_TIMEOUT).
Recommended: libcurl >= 8.11.1 for fully async file upload support.
On older versions, file uploads (CURLFile) fall back to synchronous read()
inside the read callback. This is safe for local files but blocks the event loop
briefly during each read. Downloads and user write callbacks work correctly on
all versions.
Intermittent timeout on file uploads (~20% failure rate):
Operation timed out after 5000 milliseconds with 0 bytes received
Multiple bugs in libcurl's PAUSE/unpause mechanism prevent the transfer from
being driven after curl_easy_pause(CURLPAUSE_CONT):
-
timer_lastcall/last_expire_tsoptimization —Curl_update_timer()skips the timer callback when the new expire timestamp matches the cached one. Fixed in curl#15627 (8.11.1), but the fix was removed during intermediate refactors (present in 8.5.0, absent in 8.6–8.10, re-added in 8.11.1). -
tempcountguard oncselect_bits— Incurl_easy_pause(),data->conn->cselect_bitsis only set whendata->state.tempcount == 0. If any response data arrived while the transfer was paused,tempcount > 0andcselect_bitsis never set. Withoutcselect_bits, the transfer is not processed even when timeouts fire correctly. Fixed in 8.11.1+ wherecselect_bitswas replaced withdata->state.select_bits(always set, notempcountguard). -
CURLINFO_ACTIVESOCKETunreliable during transfer — ReturnsCURL_SOCKET_BAD(-1) in themulti_socketAPI becauselastconnect_idis not set until the transfer completes. Cannot be used to drive the socket directly.
| Approach | Result |
|---|---|
curl_multi_socket_action(CURL_SOCKET_TIMEOUT) after unpause |
~80% pass |
Manual curl_timer_cb(multi, 0, NULL) to force 0ms timer |
~94% pass |
CURLINFO_ACTIVESOCKET + CURL_CSELECT_IN|OUT |
~92% pass (socket sometimes BAD) |
Track socket via curl_socket_cb + direct socket action |
~82–92% pass (socket removed from sockhash during pause) |
curl_multi_perform() |
~74% pass (must not mix with multi_socket API) |
For libcurl < 8.11.1, the file upload read callback (curl_async_read_cb)
uses synchronous read() instead of the async PAUSE/unpause pattern.
This completely avoids the bug — no PAUSE means no broken unpause.
#if LIBCURL_VERSION_NUM < 0x080B01
// Synchronous read — safe for local files
const ssize_t n = read(fd, buffer, requested);
#else
// Async PAUSE/unpause pattern
return CURL_READFUNC_PAUSE;
#endifCompile-time check via LIBCURL_VERSION_NUM. Zero runtime overhead.
With libcurl >= 8.11.1, the async path is used and works 100% reliably.
curl_exec() hangs indefinitely when connecting to unreachable hosts (e.g. 192.0.2.1),
even with CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT set. The timeout never fires
in the multi_socket API.
A regression introduced in libcurl 8.10.0 causes CURLMOPT_TIMERFUNCTION to not be
called again after curl_multi_socket_action(CURL_SOCKET_TIMEOUT). The sequence:
- curl registers a connect socket and requests a timeout timer (e.g. 2000ms)
- Timer fires →
curl_multi_socket_action(CURL_SOCKET_TIMEOUT)is called - curl returns
running_handles=1— does not complete the transfer - curl does not call
CURLMOPT_TIMERFUNCTIONto request a new timer - The socket never becomes writable (host unreachable) → deadlock
In working versions (e.g. 8.5.0), step 4 correctly re-arms the timer with a short
interval (~35ms), which fires again and completes the transfer with CURLE_OPERATION_TIMEDOUT.
- Broken: libcurl 8.10.0 – 8.11.0
- Working: libcurl ≤ 8.9.1, libcurl ≥ 8.11.1
- Related: curl#15154 — "curl_multi still breaks in 8.10.1 with libev"
- Related: curl#15639 — Regression in multi-curl async calls (8.9.1 → 8.10.0/8.11.0)
- Fix: curl#15627 — merged into curl 8.11.1
Avoid libcurl 8.10.x for any multi_socket-based integration. Use libcurl 8.5.0
(as in CI) or >= 8.11.1.
- curl#15627 — Fix for
CURLMOPT_TIMERFUNCTIONnot being called (merged Nov 2024, curl 8.11.1) - curl#15154 — curl_multi breaks in 8.10.1 with libev
- curl#15639 — Regression in multi-curl async calls (8.9.1 → 8.10.0/8.11.0)
- curl#5299 —
CURLINFO_ACTIVESOCKETreliability issues - libcurl
multi_socketAPI: https://curl.se/libcurl/c/libcurl-multi.html - libcurl pause/unpause: https://curl.se/libcurl/c/curl_easy_pause.html
ext/curl/curl_async.c— Async cURL implementation (read/write callbacks, unpause logic)ext/curl/curl_async.h— Struct definitions and public APIext/curl/interface.c—build_mime_structure_from_hash()registers the async read callbackext/curl/curl_private.h—mime_data_cb_arg_tstruct with async state