Make 5x faster outbound requests in Laravel
November 22, 2024
Laravel Octane is well-known for speeding up your app by booting it once and serving multiple requests from that same booted application. We're not here to talk about that. Instead, I've got a weird little trick using Laravel Octane that can make your outbound HTTP requests up to 5x times faster! (Or 10x. Yes, really.)
Let's dive into how you can reuse connections in Octane to speed things up.
Prefer to watch instead of read? Check out the video version of this article on YouTube: Make faster outbound requests with Laravel
The problem: repeated connection setup
Imagine your app is hitting a weather API and every time it does so, it sets up a fresh connection. That means going through DNS lookups, TLS handshakes, and more. This connection setup adds up, and if you're doing it repeatedly, it's a real bottleneck.
Here's a simple example application I've set up that calls the weather API using Guzzle. We don't care so much about the
API response, we want a little bit more information from Guzzle about what's happening, so we set the debug
option
to true
:
// web.phpuse Illuminate\Support\Facades\Route; Route::get('/', function () { // (Don't worry I've rolled my API key) $url = 'https://api.weather.com/v1/current.json?key=1c49ed5f9e4042f0abc205341242504&q=75238'; $client = new \GuzzleHttp\Client; $client->get($url, [ 'debug' => true ]);})
When I first looked at the Guzzle debug output, I noticed something interesting after the first request, it said:
* Connection #0 to host api.weather.com left intact
Could I reuse that connection for future requests in Octane? In traditional PHP, that wouldn't be possible since each request is its own universe. But with Octane? Yes, we can!
The solution: reusing connections in Octane
With Laravel Octane, your application is booted once and kept alive in memory, which means we can keep static variables around between requests. This is a game-changer when it comes to reusing a Guzzle client.
Here's an example of how it works:
We create a new Guzzle client and assign it as a static variable. In Octane, since the app stays booted, that Guzzle client sticks around for future requests.
// ApiClient.phpnamespace App; use GuzzleHttp\Client; class ApiClient{ private static $client = null; public function createClient(): Client { return static::$client ??= new Client(); }}
Now, instead of creating a fresh Guzzle client for every request, we use the ApiClient
class to create a client. If
the client doesn't exist, we create a new one. If it does exist, we can reuse it!
// web.phpuse Illuminate\Support\Facades\Route; Route::get('/', function () { $url = 'https://api.weather.com/v1/current.json?key=1c49ed5f9e4042f0abc205341242504&q=75238'; $client = new \GuzzleHttp\Client; $client = (new \App\ApiClient)->createClient(); $client->get($url, [ 'debug' => true ]);})
When we make two requests and inspect the Guzzle debug output, we see that the connection is reused:
* Connection #0 to host api.weather.com left intact // First request* Re-using existing connection with host api.weather.com // Second request
How cool is that! So we don't have to go through the whole negotiation process again and again. We can reuse the connection that was already established.
How much faster is it?
We can use Laravel's Benchmark class to compare the speed of two different approaches:
- Fresh Client: This creates a new Guzzle client and makes a new connection for each request.
- Cached Client: This reuses the existing Guzzle client and keeps the connection open.
Here's the benchmark code:
// web.phpuse Illuminate\Support\Facades\Route; Route::get('/', function () { $url = 'https://api.weather.com/v1/current.json?key=1c49ed5f9e4042f0abc205341242504&q=75238'; // Fresh Client $fresh = \Illuminate\Support\Benchmark::measure(function () use ($url) { $client = new \GuzzleHttp\Client; $client->get($url); }); // Cached Client $cached = \Illuminate\Support\Benchmark::measure(function () use ($url) { $client = (new \App\ApiClient())->createClient(); $client->get($url); }); echo "Fresh: $fresh, Cached: $cached";})
When we run this code the first time, both clients will take about the same amount of time. That's because the first request has to set up the connection.
The first request: Fresh Client: 333 milliseconds Cached Client: 292 milliseconds
But on subsequent requests, we see a significant speed difference. The cached client becomes much faster!
The second request: Fresh Client: 293 milliseconds Cached Client: 32 milliseconds
That's over 9x faster!
The takeaway? Reusing the same client and connection cuts out the overhead of repeatedly establishing new connections. If your app makes a lot of external requests (like calling APIs), this can result in significant speed improvements.
When to use this trick
If your application makes frequent connections to third-party services, you should absolutely consider this trick. By keeping your Guzzle client as a static variable with Laravel Octane, you can drastically reduce the time spent setting up connections.
Beyond Octane...
There is one other scenario where your application stays booted in-memory where this trick might help you out: queues. When you run your queue worker in production, Laravel keeps an instance of your application booted and processes multiple jobs through it. You can utilize this same trick in that scenario!
YouTube video
Check out the video version of this article on YouTube!