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.php
use 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
]);
})
Code highlighting powered by torchlight.dev (A service I created!)

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.php
namespace 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.php
use 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:

  1. Fresh Client: This creates a new Guzzle client and makes a new connection for each request.
  2. Cached Client: This reuses the existing Guzzle client and keeps the connection open.

Here's the benchmark code:

// web.php
use 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";
})
Code highlighting powered by torchlight.dev (A service I created!)

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!

Me

Thanks for reading! My name is Aaron and I write, make videos , and generally try really hard .

If you ever have any questions or want to chat, I'm always on Twitter.

If you want to give me money (please do), you can buy my course on SQLite at HighPerformanceSQLite.com or my course on screencasting at Screencasting.com . On the off chance you're a sophomore at Texas A&M University, you can buy my accounting course at acct229.com .

You can find me on YouTube on my personal channel . If you love podcasts, I got you covered. You can listen to me on Mostly Technical .