Let client-side caching work for you. Browsers and API clients respect cache headers:
class AddCachingHeadersMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// GET requests can be cached by clients
if ($request->getMethod() === 'GET' && $response->status() === 200) {
$response->header('Cache-Control', 'public, max-age=3600');
$response->header('ETag', hash('sha256', $response->getContent()));
}
// Write operations: don't cache
if (in_array($request->getMethod(), ['POST', 'PUT', 'DELETE'])) {
$response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
}
return $response;
}
}
HTTP caching lets clients avoid making the same request twice. They check the ETag, see the response hasn't changed, and use their cached copy.
Database Connection Pooling
For high-concurrency APIs, connection pooling helps:
// config/database.php
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT'),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
// Connection pooling
'options' => [
PDO::ATTR_PERSISTENT => true,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET sql_mode='STRICT_TRANS_TABLES'",
],
'pool' => [
'min' => 2,
'max' => 10,
],
],
Request/Response Truncation
Store large payloads with compression:
class ApiRequestLog extends Model
{
protected $table = 'api_request_logs';
protected function requestBody(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value ? gzuncompress($value) : null,
set: fn (?string $value) => $value ? gzcompress($value) : null,
);
}
// Truncate large bodies before storing
protected static function booted()
{
static::creating(function ($model) {
if (strlen($model->request_body ?? '') > 10000) {
$model->request_body = substr($model->request_body, 0, 10000);
}
});
}
}
Pagination for Large Results
Never return all results at once:
// config/pagination.php defaults
'per_page' => 15,
// In controller
public function index(Request $request): Response
{
$domains = Domain::paginate($request->input('per_page', 15));
return response()->json([
'data' => $domains->items(),
'pagination' => [
'current_page' => $domains->currentPage(),
'per_page' => $domains->perPage(),
'total' => $domains->total(),
'last_page' => $domains->lastPage(),
],
]);
}
// Usage: GET /api/licenses?page=2&per_page=25
Performance Monitoring
Track slow queries and requests:
// In boot method
DB::listen(function (QueryExecuted $query) {
// Log queries taking >1 second
if ($query->time > 1000) {
Log::warning('Slow query detected', [
'query' => $query->sql,
'bindings' => $query->bindings,
'time_ms' => $query->time,
]);
}
});
// Track request duration
class LogApiRequestsMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$start = microtime(true);
$response = $next($request);
$duration = round((microtime(true) - $start) * 1000);
// Log slow requests
if ($duration > 500) {
Log::warning('Slow API request', [
'path' => $request->getPathInfo(),
'duration_ms' => $duration,
]);
}
return $response;
}
}
Performance Checklist
- ✅ Use eager loading (
with()) - ✅ Index frequently queried columns
- ✅ Cache expensive operations
- ✅ Compress responses (gzip)
- ✅ Paginate large result sets
- ✅ Use database connection pooling
- ✅ Monitor slow queries
- ✅ Select only needed columns
- ✅ Use CDN for static assets
- ✅ Profile regularly
Chapter 5 Summary
Performance fundamentals:
- Compression - Gzip responses for 90% size reduction
- Eager Loading - Prevent N+1 database queries
- Caching - Cache expensive operations with expiration
- HTTP Caching Headers - Let clients cache responses
- Monitoring - Catch slow queries and endpoints
- Database Indexing - Index frequently filtered columns
- Pagination - Never load all records at once
Pay attention to the slowest part of your request, whether it is the database, an external API, or computation, and optimize the bottleneck first.
Performance is not a feature you add later. It's a characteristic of good architecture.