skip to content

In PHP 8.5, what does curl_share_init_persistent() let cURL reuse across requests, and why does it reject CURL_LOCK_DATA_COOKIE?

level: seniorimportance: nice to knowfreq 14%

answer

  1. handles die at request end
  2. share handle plus CURLOPT_SHARE
  3. DNS, SSL session, connections, PSL
  4. same option set returns the same handle
  5. cookies would leak between users

basics

~20 s

curl_share_init_persistent() (PHP 8.5) returns a CurlSharePersistentHandle that outlives the request, so a worker can reuse DNS results, TLS sessions and open connections. Cookies are rejected with a ValueError because sharing them across requests could mix one user's cookies into another's calls.

solid answer

~40 s

Normally everything cURL learns (DNS answers, TLS sessions, keep-alive connections) dies with the handles at the end of the PHP request, so every request pays a fresh handshake. A share handle, attached with `CURLOPT_SHARE`, lets handles pool that data. `curl_share_init()` creates one that lives for the request only. PHP 8.5 adds `curl_share_init_persistent(array $share_options)`. It returns a `CurlSharePersistentHandle` kept in the worker process and reused by later requests that ask for the same set of `CURL_LOCK_DATA_*` options. Allowed options are `DNS`, `SSL_SESSION`, `CONNECT` and `PSL`. `CURL_LOCK_DATA_COOKIE` throws a `ValueError`, because a persisted cookie jar would carry one user's session cookies into the next user's requests. An empty array or an unknown value also throws a `ValueError`.

code

php · 19 lines
php
<?php
declare(strict_types=1);

// PHP 8.5: survives into later requests served by this worker.
$sh = curl_share_init_persistent([
    CURL_LOCK_DATA_DNS,
    CURL_LOCK_DATA_SSL_SESSION,
    CURL_LOCK_DATA_CONNECT,
]);

$ch = curl_init('https://rates.example.test/v1/quote');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 5,
    CURLOPT_SHARE          => $sh,
]);
$body = curl_exec($ch); // may reuse a connection opened by an earlier request

// curl_share_init_persistent([CURL_LOCK_DATA_COOKIE]) would throw ValueError.

go deeper

for a junior

Recall that cURL handles normally die with the request, and that PHP 8.5 added a share handle that survives it to reuse DNS and connections.

for a middle

Explain CURLOPT_SHARE, the allowed CURL_LOCK_DATA_* options, the reuse-by-option-set rule, and the ValueError for cookies, empty arrays and unknown values.

for a senior

Judge where persistent sharing pays off (handshake-heavy calls from long-lived workers), reason about per-process scope, and explain the cross-user leak that motivates the cookie ban.

for a principal

Weigh connection reuse against isolation: persisted state in a share-nothing runtime is a deliberate exception, so limit it to identity-free data and document it for upgrades.

## The problem it solves PHP's usual model is **share-nothing**: each request starts clean and everything it allocated, cURL handles included, is freed at the end. For outbound HTTP that means every request that calls an API (a shipping-rates service, say) resolves DNS again, opens a new TCP connection and performs a full TLS handshake. For an API a few milliseconds away, the handshake can cost more than the call itself. libcurl has a **share interface** that lets several easy handles pool some of their state. PHP has exposed it for years through `curl_share_init()` and `curl_share_setopt()` with `CURLSHOPT_SHARE`. But a normal share handle is an ordinary object, so it too is destroyed at the end of the request. It helps within one request only. ## What PHP 8.5 adds `curl_share_init_persistent(array $share_options): CurlSharePersistentHandle` creates, or finds, a share handle that is **not destroyed at the end of the request**. It lives in the PHP process, so in an FPM worker it survives from one request to the next that the same worker serves. Key points, from the manual and `ext/curl/share.c`: - `$share_options` is a non-empty array of `CURL_LOCK_DATA_*` constants. There is no separate `setopt` step. - If a persistent handle with the **same set** of options already exists in the process, it is returned instead of a new one. - The returned object is a `CurlSharePersistentHandle`, a final class with a public readonly `array $options`. It cannot be constructed with `new` or cloned. - A `CurlHandle` uses it through `curl_setopt($ch, CURLOPT_SHARE, $sh)`, like a normal share handle. ## Which data may be shared | Constant | What is reused | Allowed persistently | |---|---|---| | `CURL_LOCK_DATA_DNS` | resolved host names | yes | | `CURL_LOCK_DATA_SSL_SESSION` | TLS session tickets for faster resumption | yes | | `CURL_LOCK_DATA_CONNECT` | open keep-alive connections | yes | | `CURL_LOCK_DATA_PSL` | the public suffix list | yes | | `CURL_LOCK_DATA_COOKIE` | the cookie jar | **no: `ValueError`** | The errors are specific: an empty array throws `ValueError`, an unknown constant throws `ValueError`, a non-integer entry throws `TypeError`, and `CURL_LOCK_DATA_COOKIE` throws `ValueError` with a message that sharing cookies across PHP requests is unsafe. ## Why cookies are excluded Consecutive requests to one worker usually come from **different users**. If the cookie jar persisted: 1. user A's request calls an upstream that sets a session cookie for A; 2. the cookie is stored in the shared jar inside the worker; 3. user B's next request, served by the same worker, calls the same upstream; 4. libcurl attaches A's cookie, and B acts with A's session upstream. That is a cross-user data leak created purely by an optimisation. DNS entries, TLS sessions and idle connections carry no per-user identity, so they are safe to reuse; a cookie is identity. ## Related handle changes to place it - Since PHP 8.0 all three handle kinds are objects: `CurlHandle`, `CurlMultiHandle`, `CurlShareHandle`. - PHP 8.5 deprecates `curl_share_close()` along with `curl_close()`, as both have done nothing since 8.0. - A persistent share handle is per **process**. Separate FPM workers each hold their own, and nothing is shared between servers. ## When to use it - Good fit: many short requests calling the same few HTTPS APIs, where handshakes dominate latency. - Check first: whether your process model keeps workers alive between requests (FPM and long-lived runtimes do; a CLI script that exits does not benefit). - Watch: stale DNS after an upstream moves, and upstreams that close idle connections, which libcurl detects and reopens. ## How to tell whether it works The effect shows up in the timings that `curl_getinfo()` reports. On a request that reuses a pooled connection, the connect and TLS phases are close to zero, so `CURLINFO_CONNECT_TIME` and the handshake part of the total drop sharply compared with a cold request. PHP 8.5 also added `CURLINFO_CONN_ID` (with libcurl 8.2.0 or later), which identifies the connection a transfer used; seeing the same ID across requests served by one worker confirms the reuse. If every request still shows a full handshake, check that the option set is identical each time, since a different set gets a different persistent handle, and that the worker is not being recycled after every request.

  • How does a normal curl_share_init() handle differ from the PHP 8.5 persistent one?
    A `CurlShareHandle` from `curl_share_init()` is an ordinary object, configured with `curl_share_setopt()` and freed when the request ends, so it pools data between handles within one request only. `curl_share_init_persistent()` takes its options up front, returns a `CurlSharePersistentHandle` that the process keeps, and hands the same handle back for the same option set in later requests.
  • Does a persistent share handle let two FPM workers share one connection?
    No. The handle is stored in the process, so each FPM worker has its own and pools only its own connections and DNS entries. Nothing crosses processes or servers. The benefit comes from a worker serving many requests in sequence, each reusing what the previous one set up.

saying these in an interview costs you the question

  • Persistent share handles are shared by all FPM workers on the server.
  • curl_share_init() handles already survive between requests.
  • Sharing cookies persistently is allowed if every user calls the same API.
  • The persistent handle is created with new CurlSharePersistentHandle().
  • curl_share_init_persistent() has existed since PHP 8.0.