Introduction

PHP's file_get_contents() with stream_context_create() can make HTTP requests without cURL, but timeout configuration is subtle and often misunderstood. The timeout option in the HTTP context only sets the connection timeout, not the read timeout. The read timeout is controlled by PHP's default_socket_timeout ini setting (default 60 seconds). When both are not configured, requests can hang for the full default_socket_timeout even when a short timeout is specified, causing slow application response times and resource exhaustion under load.

Symptoms

bash
# timeout set to 2 seconds but request takes 60 seconds
$context = stream_context_create(['http' => ['timeout' => 2]]);
file_get_contents('http://slow-api.example.com/data', false, $context);
# Hangs for 60 seconds (default_socket_timeout), not 2!

Or:

bash
file_get_contents(): Failed to open stream: Connection timed out
# After 60 seconds, not the expected timeout value

Common Causes

  • timeout option only sets connection timeout: Does not affect read timeout
  • default_socket_timeout not overridden: PHP default of 60 seconds applies to reads
  • Timeout value type: Timeout expects integer, float values truncated
  • HTTPS stream not configured: SSL context not set for HTTPS requests
  • User-Agent not set: Some servers reject requests without User-Agent header
  • Error handling missing: stream_get_meta_data not checked for timeout

Step-by-Step Fix

Step 1: Configure both connection and read timeouts

```php function httpGet(string $url, int $timeout = 10): string|false { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, // Connection timeout 'header' => [ 'User-Agent: MyApp/1.0', 'Accept: application/json', ], 'ignore_errors' => true, // Get response even on error status ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]);

// Set read timeout (applies to the stream, not just connection) $defaultSocketTimeout = ini_get('default_socket_timeout'); ini_set('default_socket_timeout', (string) $timeout);

try { $result = file_get_contents($url, false, $context);

// Check if timeout occurred if ($context && $result !== false) { $meta = stream_get_meta_data($result); // Note: stream_get_meta_data works on stream resources, not string results }

return $result; } finally { // Restore original timeout ini_set('default_socket_timeout', $defaultSocketTimeout); } } ```

Step 2: Safer approach with stream functions

```php function httpGetSafe(string $url, int $timeout = 10): array { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'header' => ['User-Agent: MyApp/1.0'], 'ignore_errors' => true, ], ]);

$stream = @fopen($url, 'r', false, $context); if ($stream === false) { return ['error' => 'Failed to open stream', 'status' => 0]; }

// Set read timeout on the stream stream_set_timeout($stream, $timeout);

$content = stream_get_contents($stream); $meta = stream_get_meta_data($stream); $status = $meta['wrapper_data'][0] ?? ''; // HTTP/1.1 200 OK

fclose($stream);

if ($meta['timed_out']) { return ['error' => 'Request timed out', 'status' => 0]; }

preg_match('/HTTP\/\d\.\d\s+(\d+)/', $status, $matches); $statusCode = (int) ($matches[1] ?? 0);

return [ 'content' => $content, 'status' => $statusCode, 'headers' => $meta['wrapper_data'], ]; } ```

Prevention

  • Set both 'timeout' in stream context AND default_socket_timeout for reads
  • Use stream_set_timeout() on the stream resource for per-request read timeout
  • Always set User-Agent header to avoid server rejections
  • Use ignore_errors => true to read error response bodies
  • Verify SSL certificates with verify_peer and verify_peer_name options
  • Prefer cURL or Guzzle for complex HTTP requests with proper timeout handling
  • Add timeout monitoring to detect slow external dependencies