Slow APIs lose users. There's no way around it. A 500ms API response feels instant. A 3-second response makes users wonder if something's broken.
"Make it fast later" doesn't work. Optimizing a slow architecture is exponentially harder than building a fast one from the start. Performance debt is like regular debt, it compounds.
In this chapter, we'll talk about the practical optimizations that matter: database query optimization, caching, compression, and monitoring.
Compression: Make Responses Smaller
Simple idea: compress responses with gzip. A 100KB JSON response becomes 10KB. Your users download 90% less data. Everyone wins.
class GzipResponseMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Client must support gzip encoding
if (!str_contains($request->header('Accept-Encoding'), 'gzip')) {
return $response;
}
// Compress response
$response->setContent(gzencode($response->getContent(), 9));
$response->header('Content-Encoding', 'gzip');
return $response;
}
}
// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->api(append: [
GzipResponseMiddleware::class,
]);
})
Gzip is transparent to the client. Modern browsers and API clients automatically decompress. You get ~90% size reduction for free.
Query Optimization: Avoid N+1
The classic database mistake: loading a list of domains, then looping through them to get the user for each one. That's N database queries just to get the user names!
Fix it with eager loading:
// ❌ Bad: N+1 queries (disaster!)
$domains = Domain::all(); // 1 query
foreach ($domains as $domain) {
echo $domain->user->name; // +1 query per domain = N queries
}
// Total: 1 + N queries where N = number of domains
// ✅ Good: Eager loading (one query total)
$domains = Domain::with('user')->get();
foreach ($domains as $domain) {
echo $domain->user->name; // Already loaded
}
// Total: 2 queries (domains + users)
// ✅ Even Better: Load only needed columns
$domains = Domain::with([
'user' => fn ($q) => $q->select('id', 'name'),
'records' => fn ($q) => $q->latest()->limit(5),
])->get();
// Load only the columns and relationships you actually use
This is one of the highest-impact optimizations you can make. Loads of slow APIs are slow because of this mistake.
Caching: Store Expensive Computations
Database queries take time. API calls to external services take a lot of time. Cache frequently accessed data:
class DomainService
{
// Cache all domains for 1 hour
public function licenses(): Collection
{
$cached = Cache::get('domains:all');
if ($cached) {
return $cached;
}
$domains = Domain::all();
Cache::put('domains:all', $domains, 3600);
return $domains;
}
// Or use Cache::remember for convenience
public function licensesByUser(Bearer $bearer): Collection
{
return Cache::remember(
"domains:bearer:{$bearer->id}",
3600,
fn () => $bearer->domains()->get()
);
}
// Invalidate cache when domain changes
public function updateDomain(Domain $domain, array $data): void
{
$domain->update($data);
// Clear affected caches
Cache::forget('domains:all');
Cache::forget("domains:bearer:{$domain->bearer_id}");
}
}
Caching strategy:
- Identify slow operations (queries, API calls)
- Cache the result
- Invalidate when the underlying data changes
- Use tags to group related cache entries