Comprehensive guide to building high-performance PHP APIs with token bucket rate limiting, multi-layer caching architecture, response compression, and streaming for large datasets with complete working code examples.
PHP Problem Solving: Building High-Performance APIs with Rate Limiting and Caching
Building scalable and performant APIs requires more than just functional code. Modern PHP applications must handle high traffic loads, prevent abuse through rate limiting, and minimize latency through intelligent caching. This comprehensive tutorial explores advanced techniques for building production-ready APIs that can handle enterprise-level demands while maintaining security and reliability.
FOCUS AREA: This tutorial covers API rate limiting strategies, multi-layer caching architectures, request optimization, load balancing considerations, and complete working implementations for building high-performance PHP APIs.
Problem 1: Implementing Token Bucket Rate Limiting
Challenge: You need to implement a fair and efficient rate limiting system that allows burst traffic while preventing abuse, with support for different limits per user tier and API endpoint.
<?php
/**
* Token Bucket Rate Limiting Implementation
* Problem: Fair and efficient API rate limiting
*/
class TokenBucketRateLimiter {
private $storage;
private int $defaultCapacity;
private float $defaultRefillRate;
public function __construct($storage, int $defaultCapacity = 100, float $defaultRefillRate = 10) {
$this->storage = $storage;
$this->defaultCapacity = $defaultCapacity;
$this->defaultRefillRate = $defaultRefillRate;
}
/**
* Check if request is allowed using token bucket algorithm
*/
public function isAllowed(string $identifier, int $cost = 1, ?array $config = null): bool {
$capacity = $config['capacity'] ?? $this->defaultCapacity;
$refillRate = $config['refill_rate'] ?? $this->defaultRefillRate;
$key = "rate_limit:{$identifier}";
$now = microtime(true);
// Get current bucket state
$bucket = $this->storage->get($key);
if (!$bucket) {
// New bucket - start full
$bucket = [
'tokens' => $capacity - $cost,
'last_refill' => $now
];
$this->storage->set($key, $bucket, 3600);
return true;
}
// Calculate tokens to add based on time elapsed
$timeElapsed = $now - $bucket['last_refill'];
$tokensToAdd = $timeElapsed * $refillRate;
// Refill bucket (capped at capacity)
$bucket['tokens'] = min($capacity, $bucket['tokens'] + $tokensToAdd);
$bucket['last_refill'] = $now;
// Check if enough tokens for request
if ($bucket['tokens'] >= $cost) {
$bucket['tokens'] -= $cost;
$this->storage->set($key, $bucket, 3600);
return true;
}
// Not enough tokens - update last_refill but don't deduct
$this->storage->set($key, $bucket, 3600);
return false;
}
/**
* Get rate limit status for client
*/
public function getStatus(string $identifier, ?array $config = null): array {
$capacity = $config['capacity'] ?? $this->defaultCapacity;
$refillRate = $config['refill_rate'] ?? $this->defaultRefillRate;
$key = "rate_limit:{$identifier}";
$now = microtime(true);
$bucket = $this->storage->get($key);
if (!$bucket) {
return [
'allowed' => true,
'remaining' => $capacity,
'reset_time' => $now + ($capacity / $refillRate),
'limit' => $capacity
];
}
// Calculate current tokens
$timeElapsed = $now - $bucket['last_refill'];
$currentTokens = min($capacity, $bucket['tokens'] + ($timeElapsed * $refillRate));
// Calculate reset time (when bucket will be full)
$tokensNeeded = $capacity - $currentTokens;
$resetTime = $now + ($tokensNeeded / $refillRate);
return [
'allowed' => $currentTokens >= 1,
'remaining' => (int)floor($currentTokens),
'reset_time' => $resetTime,
'limit' => $capacity
];
}
}
/**
* Redis-backed storage for distributed rate limiting
*/
class RedisRateLimitStorage {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
public function get(string $key): ?array {
$data = $this->redis->get($key);
return $data ? json_decode($data, true) : null;
}
public function set(string $key, array $value, int $ttl): void {
$this->redis->setex($key, $ttl, json_encode($value));
}
}
// Usage example
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$storage = new RedisRateLimitStorage($redis);
$rateLimiter = new TokenBucketRateLimiter($storage, 100, 10); // 100 tokens, refill 10/sec
// API endpoint protection
$clientId = $_SERVER['HTTP_X_API_KEY'] ?? $_SERVER['REMOTE_ADDR'];
if (!$rateLimiter->isAllowed($clientId, 1)) {
http_response_code(429);
header('Content-Type: application/json');
$status = $rateLimiter->getStatus($clientId);
echo json_encode([
'error' => 'Rate limit exceeded',
'retry_after' => ceil($status['reset_time'] - microtime(true))
]);
exit;
}
// Set rate limit headers
$status = $rateLimiter->getStatus($clientId);
header('X-RateLimit-Limit: ' . $status['limit']);
header('X-RateLimit-Remaining: ' . $status['remaining']);
header('X-RateLimit-Reset: ' . ceil($status['reset_time']));
// Process API request
echo json_encode(['data' => 'API response']);
?>
Problem 2: Multi-Layer Caching Architecture
Challenge: You need to implement a caching system that combines in-memory, file-based, and distributed caching layers to optimize performance while handling cache invalidation and consistency across multiple servers.
<?php
/**
* Multi-Layer Caching Implementation
* Problem: Optimized caching with multiple storage layers
*/
class MultiLayerCache {
private array $layers;
private array $ttlMap;
public function __construct(array $config) {
$this->layers = [];
$this->ttlMap = $config['ttl_map'] ?? [
'memory' => 300, // 5 minutes
'file' => 3600, // 1 hour
'redis' => 86400 // 24 hours
];
// Initialize cache layers in priority order
if (isset($config['memory']) && $config['memory']['enabled']) {
$this->layers['memory'] = new MemoryCacheLayer($config['memory']['size'] ?? 1000);
}
if (isset($config['file']) && $config['file']['enabled']) {
$this->layers['file'] = new FileCacheLayer($config['file']['path'] ?? '/tmp/cache');
}
if (isset($config['redis']) && $config['redis']['enabled']) {
$this->layers['redis'] = new RedisCacheLayer($config['redis']['connection']);
}
}
/**
* Get value from cache with layer cascade
*/
public function get(string $key) {
// Try each layer in order
foreach ($this->layers as $layerName => $layer) {
$value = $layer->get($key);
if ($value !== null) {
// Backfill to faster layers
$this->backfill($key, $value, $layerName);
return $value;
}
}
return null;
}
/**
* Store value in all cache layers
*/
public function set(string $key, $value, ?int $ttl = null): void {
foreach ($this->layers as $layerName => $layer) {
$layerTtl = $ttl ?? $this->ttlMap[$layerName];
$layer->set($key, $value, $layerTtl);
}
}
/**
* Invalidate cache key across all layers
*/
public function delete(string $key): void {
foreach ($this->layers as $layer) {
$layer->delete($key);
}
}
/**
* Pattern-based invalidation
*/
public function deletePattern(string $pattern): void {
foreach ($this->layers as $layer) {
$layer->deletePattern($pattern);
}
}
/**
* Backfill value to faster cache layers
*/
private function backfill(string $key, $value, string $foundInLayer): void {
$foundPriority = array_search($foundInLayer, array_keys($this->layers));
$layerNames = array_keys($this->layers);
// Fill layers with higher priority (earlier in array)
for ($i = 0; $i < $foundPriority; $i++) {
$layerName = $layerNames[$i];
$this->layers[$layerName]->set($key, $value, $this->ttlMap[$layerName]);
}
}
}
/**
* APCu-backed in-memory cache layer
*/
class MemoryCacheLayer {
private int $maxSize;
private array $keys = [];
public function __construct(int $maxSize = 1000) {
$this->maxSize = $maxSize;
}
public function get(string $key) {
return apcu_fetch($key);
}
public function set(string $key, $value, int $ttl): void {
apcu_store($key, $value, $ttl);
}
public function delete(string $key): void {
apcu_delete($key);
}
public function deletePattern(string $pattern): void {
$info = apcu_cache_info();
foreach ($info['cache_list'] as $entry) {
if (fnmatch($pattern, $entry['info'])) {
apcu_delete($entry['info']);
}
}
}
}
/**
* File-based cache layer
*/
class FileCacheLayer {
private string $path;
public function __construct(string $path) {
$this->path = rtrim($path, '/');
if (!is_dir($this->path)) {
mkdir($this->path, 0755, true);
}
}
public function get(string $key) {
$file = $this->getFilePath($key);
if (!file_exists($file)) {
return null;
}
$data = unserialize(file_get_contents($file));
if ($data['expires'] < time()) {
unlink($file);
return null;
}
return $data['value'];
}
public function set(string $key, $value, int $ttl): void {
$file = $this->path . '/' . $this->sanitizeKey($key) . '.cache';
$data = [
'expires' => time() + $ttl,
'value' => $value
];
file_put_contents($file, serialize($data), LOCK_EX);
}
public function delete(string $key): void {
$file = $this->getFilePath($key);
if (file_exists($file)) {
unlink($file);
}
}
public function deletePattern(string $pattern): void {
foreach (glob($this->path . '/*.cache') as $file) {
$key = basename($file, '.cache');
if (fnmatch($pattern, $key)) {
unlink($file);
}
}
}
private function getFilePath(string $key): string {
return $this->path . '/' . $this->sanitizeKey($key) . '.cache';
}
private function sanitizeKey(string $key): string {
return preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
}
}
/**
* Redis cache layer
*/
class RedisCacheLayer {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
public function get(string $key) {
$value = $this->redis->get($key);
return $value !== false ? unserialize($value) : null;
}
public function set(string $key, $value, int $ttl): void {
$this->redis->setex($key, $ttl, serialize($value));
}
public function delete(string $key): void {
$this->redis->del($key);
}
public function deletePattern(string $pattern): void {
$keys = $this->redis->keys($pattern);
if (!empty($keys)) {
$this->redis->del(...$keys);
}
}
}
// Usage example
$cache = new MultiLayerCache([
'memory' => ['enabled' => true, 'size' => 1000],
'file' => ['enabled' => true, 'path' => '/tmp/api-cache'],
'redis' => ['enabled' => true, 'connection' => $redis]
]);
// Cache expensive database query
$userData = $cache->get('user:123');
if ($userData === null) {
$userData = fetchUserFromDatabase(123); // Expensive operation
$cache->set('user:123', $userData, 600); // 10 minute TTL
}
// Invalidate user cache on update
function updateUser(int $id, array $data): void {
updateDatabase($id, $data);
$GLOBALS['cache']->delete("user:{$id}");
}
?>
Performance Optimization Tips
Cache Warming: Pre-populate cache with expected hot data during low-traffic periods
Cache Stampede Protection: Use probabilistic early expiration to prevent simultaneous cache rebuilds
Conditional Requests: Implement ETag and Last-Modified headers for HTTP caching
Problem 3: API Response Compression and Streaming
<?php
/**
* API Response Optimization
* Compression, streaming, and efficient delivery
*/
class ApiResponseOptimizer {
/**
* Compress and send JSON response efficiently
*/
public function sendJsonResponse($data, int $statusCode = 200): void {
$json = json_encode($data);
$jsonLength = strlen($json);
http_response_code($statusCode);
header('Content-Type: application/json');
// Enable compression for responses > 1KB
if ($jsonLength > 1024 && $this->clientSupportsCompression()) {
$compressed = gzencode($json, 6);
header('Content-Encoding: gzip');
header('Content-Length: ' . strlen($compressed));
echo $compressed;
} else {
header('Content-Length: ' . $jsonLength);
echo $json;
}
}
/**
* Stream large datasets without memory exhaustion
*/
public function streamJsonArray(iterable $dataGenerator): void {
http_response_code(200);
header('Content-Type: application/json');
header('Transfer-Encoding: chunked');
echo '[';
$first = true;
foreach ($dataGenerator as $item) {
if (!$first) {
echo ',';
}
echo json_encode($item);
$first = false;
// Flush output buffer periodically
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
echo ']';
}
/**
* Check if client accepts compressed responses
*/
private function clientSupportsCompression(): bool {
$acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '';
return strpos($acceptEncoding, 'gzip') !== false;
}
/**
* Generate ETag for conditional requests
*/
public function generateEtag($data): string {
return md5(serialize($data));
}
/**
* Handle conditional request with ETag
*/
public function handleConditionalRequest($data, string $etag): bool {
$clientEtag = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
if ($clientEtag === $etag) {
http_response_code(304); // Not Modified
return false; // Don't send body
}
header('ETag: "' . $etag . '"');
return true; // Send full response
}
}
// Usage: Streaming large dataset
function getLargeDatasetGenerator() {
$pdo = new PDO('mysql:host=localhost;dbname=large_db', 'user', 'pass');
$stmt = $pdo->query("SELECT * FROM large_table");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
}
$optimizer = new ApiResponseOptimizer();
$optimizer->streamJsonArray(getLargeDatasetGenerator());
?>
API Performance Architecture
Building high-performance APIs requires a multi-faceted approach combining rate limiting for protection, multi-layer caching for speed, and streaming for large data. The token bucket algorithm provides fair resource allocation, while strategic caching layers balance speed against consistency. Response compression and streaming techniques minimize bandwidth and memory usage for large payloads.
Production-Ready API Development
The techniques covered in this tutorial form the foundation of scalable API architecture. Rate limiting protects your infrastructure from abuse while ensuring fair access. Multi-layer caching dramatically reduces response times and database load. Streaming and compression optimize delivery of large datasets without memory exhaustion.
Remember that performance optimization is an ongoing process requiring monitoring, profiling, and iterative improvement. Start with measured benchmarks, identify actual bottlenecks rather than assumed ones, and validate that optimizations deliver real benefits for your specific use case. The goal is not perfect performance but appropriate performance that meets user needs while maintaining system stability.
Comments (0)
No comments yet. Be the first to share your thoughts!