Buzeli
buzeliSoluções Digitais
Incidents

CURLOPT_TIMEOUT = 0: the infinite timeout freezing the payment gateway for 60 seconds

Published on April 10, 2026

CURLOPT_TIMEOUT = 0 travando gateway Pagar.me por até 60 segundos — latência p99 5,4s, max 60s, fix em 6 arquivos

The symptom that misled the diagnosis

A payment gateway started showing intermittent slowness. The behavior was unusual: server CPU at 4%, stable memory, no traffic spikes. But the p99 latency was at 5.410s and the recorded maximum reached 60.001s. In a 30-minute window, 161 out of 1,152 requests (14%) exceeded 2 seconds.

Low CPU during a slowness incident is a classic trap. It rules out intensive processing, but not blocking I/O — which consumes zero CPU while locking a thread waiting for a response from an external service.

499 errors (client disconnected before receiving a response) appeared in nginx logs, confirming that users were abandoning requests before getting a response. The payment gateway was locking PHP-FPM threads waiting for a gateway response that never came — or came too late.

The cause: CURLOPT_TIMEOUT = 0 in 6 files

Investigation of the gateway's PHP files revealed the same line in 6 different places:

Copy
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
// or equivalently:
curl_setopt($ch, CURLOPT_TIMEOUT, 0);  // no timeout

In cURL, CURLOPT_TIMEOUT = 0 does not mean 'no limit configured' — it means 'wait indefinitely'. This is counter-intuitive: zero doesn't disable the timeout, it sets the timeout to infinity.

The 6 affected files covered all critical gateway flows:

Checkout.php — standard checkout flow

CheckoutTransparent.php — transparent checkout (card)

GatewayServices.php — auxiliary services

Subscriptions.php — recurring subscription flow

SessionHandler.php — gateway session management

Refunds.php — chargebacks and refunds

Why zero appears in these files

The value 0 is frequently copied from documentation examples or legacy code where the developer wanted to say 'I don't want PHP defaults to interfere — let cURL control it'. The intent was to disable PHP's timeout (via max_execution_time) by delegating control to cURL. The actual effect was the opposite: with no timeout defined in cURL, any request to the gateway that took long — due to API instability, network latency, temporary overload — would hang indefinitely.

The 60.001s maximum is not a coincidence: it's PHP's default timeout (max_execution_time = 60s) acting as a last resort. Without it, the thread would remain locked until the process restarted.

The diagnosis: evidence in the logs

To confirm the blocking I/O hypothesis, nginx and PHP-FPM logs were cross-referenced:

Copy
# 499 errors in nginx — client disconnected before response
grep ' 499 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn

# Stuck PHP-FPM workers — process status
curl http://localhost/fpm-status?full | grep -E 'state|last request uri|request duration'

# Latency distribution of payment gateway requests
grep 'payment-gateway' /var/log/app/requests.log | awk '{print $NF}' | sort -n |   awk 'BEGIN{c=0} {a[c++]=$1} END{
    print "p90: " a[int(c*0.90)];
    print "p95: " a[int(c*0.95)];
    print "p99: " a[int(c*0.99)];
    print "max: " a[c-1]
  }'

PHP-FPM status showed workers in 'Reading headers' state for tens of seconds — waiting for gateway responses. With the pool configured for N workers, when enough threads were simultaneously stuck, new requests started queuing, worsening the slowness.

The fix: 3 files at a time, validation between each batch

Copy
// Before — infinite timeout
curl_setopt($ch, CURLOPT_TIMEOUT, 0);

// After — 5 second timeout
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

The 5-second value was chosen based on the gateway historical p99 under normal conditions (below 3s) with margin. Requests that legitimately take more than 5s with the gateway are a symptom of a problem in the gateway API — not normal operation — and should fail quickly to free the thread and return a handleable error to the user.

Important: CURLOPT_TIMEOUT affects the total cURL operation time (connection + transfer). For more granular control, also use CURLOPT_CONNECTTIMEOUT to separately limit the connection establishment time.

Copy
// Recommended complete configuration
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);  // max 3s to connect
curl_setopt($ch, CURLOPT_TIMEOUT, 5);          // max 5s total

Result

After applying the fix to all 6 files and reloading PHP-FPM (no downtime), 499 errors dropped to zero in the first hour of monitoring. The p99 returned to under 1 second under normal conditions. No payment transaction was lost during the rollout — the fix was applied file by file with validation between each deploy.

A value that looks 'disabled' (zero) that actually means 'infinite' is the kind of bug that survives in production for months — because it generates no explicit error, only intermittent slowness. The only way to find it is to actively search files that make external requests.

How to audit your code

To find all uses of CURLOPT_TIMEOUT = 0 in a PHP project:

Copy
# Find CURLOPT_TIMEOUT with value 0 in PHP files
grep -rn 'CURLOPT_TIMEOUT.*0' /var/www/html/ --include='*.php'

# Also search for the numeric constant (13 = CURLOPT_TIMEOUT)
grep -rn 'CURLOPT_TIMEOUT\b' /var/www/html/ --include='*.php' | grep -v '//.*CURLOPT_TIMEOUT'

# Check if CURLOPT_CONNECTTIMEOUT is missing from the same files
grep -rL 'CURLOPT_CONNECTTIMEOUT' $(grep -rl 'curl_setopt' /var/www/html/ --include='*.php')

Any file making external requests with cURL must explicitly have CURLOPT_TIMEOUT > 0 and CURLOPT_CONNECTTIMEOUT > 0. The absence of these settings in integrations with payment APIs, postal code lookups, notifications, or any external service is a direct availability risk.