Caching in Laravel with Speed Comparisons

Caching is one aspect of web development I am guilty of overlooking a lot and I am sure a lot us are guilty of that too. I have come to realize how important it is and I will explain the importance with Scotch as a case study.

From experience and a little observation, Scotch schedules articles daily. Therefore, articles are not (for now) released within 24 hours of the last post. It follows that data on the landing page will remain the same for 24 hours. The main point here is, it is pointless to ask the database for articles within that 24 (or to be safe 22 - 23) hour range.

Cache to the rescue! In Laravel, we can cache the results for 22 hours and when a request is made, the controller responds with a cached value until the cache time expires.

We're going to look at the basic usage of the Laravel cache and then get into a quick demo app to see just how much faster caching can make our applications.

Basic Usage of Cache in Laravel

Laravel makes it easy for us to switch out how we want caching to be generated. It is extremely easy for us to switch out the drivers. Just check out config/cache.php to see the available drivers which are:

  • apc
  • array
  • database
  • file
  • memcached
  • redis

You can then set the following line in your .env file to change the cache driver:

CACHE_DRIVER=file

You may go ahead and try these examples out without bothering about configuration, as it defaults to file.

The Cache facade exposes a lot of static methods to create, update, get, delete, and check for existence of a cache content. Let us explore some of these methods before we build a demo app.

Create/Update Cache Value

We can add or update cache values with the put() method. The method accepts 3 necessary arguments:

  • a key
  • the value
  • the expiration time in minutes

For example:

Cache::put('key', 'value', 10);

The key is a unique identifier for the cache and will be used to retrieve it when needed.

Furthermore, we can use the remember() method to automate retrieving and updating a cache. The method first checks for the key and if it has been created, returns it. Otherwise, it will create a new key with the value returned from it's closure like so:

Cache::remember('articles', 15, function() {
    return Article::all();
});

The value 15 is the number of minutes it will be cached. This way, we don't even have to do a check if a cache has expired. Laravel will do that for us and retrieve or regenerate the cache without us having to explicitly tell it to.

Retrieve Cache Value

The values of a cache can be retrieved using the get() method which just needs a key passed as argument to know which cache value to grab:

Cache::get('key');

Check for Existence

Sometimes it is very important to check if a cache key exists before we can retrieve or update it. The has() method becomes handy:

if (Cache::has('key')){
    Cache::get('key');
} else {
    Cache::put('key', $values, 10);
}

Removing Cache Value

Cache values can be removed with the forget() method and passing the key to it:

Cache::forget('key');

We can also retrieve a cache value and delete it immediately. I like referring to this one as one-time caching:

$articles = Cache::pull('key');

We can also clear the cache even before they expire from the console using:

php artisan cache:clear

Cache by Example

This is going to be a really simple demo based on my research on the time taken to process requests with or without caches. To get straight to the point, I suggest you set up a Laravel instance on your own and follow along with the tutorial.

Model and Migrations

Create a model named Article using the command below:

php artisan make:model Article -m

The -m option will automatically create a migration for us so we need not run a "create migration" command. This single command will create an App/Article.php and a database/migrations/xxxx_xx_xx_xxxxxx_create_articles_table.php file.

Update your new migration file to add two table fields:

public function up() {
    Schema::create('articles', function (Blueprint $table) {
        $table->increments('id');

        // add the following
        $table->string("title");
        $table->string("content");

        $table->timestamps();
    });
}

Now we can migrate our database using the artisan command:

php artisan migrate

Seeding our Database

Next up is to seed the articles table. In database/seeds/DatabaseSeeder.php, update the run() command with:

public function run() {
    Model::unguard();

    // use the faker library to mock some data
    $faker = Faker::create();

    // create 30 articles
    foreach(range(1, 30) as $index) {
        Article::create([
            'title' => $faker->sentence(5),
            'content' => $faker->paragraph(6)
        ]);
    }

    Model::reguard();
}

The Faker library is included in Laravel to help with quickly generating fake data. We are using the PHP's range() method to generate 30 fake columns.

Now we can seed our database with the artisan command:

php artisan db:seed

Creating an Article Controller

Next up, we can create a controller that will process the requests and caching. It will be empty for now:

php artisan make:controller ArticlesController

...then we add a route in the app/Http/routes.php which will point to the controller's index method:

Route::group(['prefix' => 'api'], function() {

    Route::get('articles', 'ArticlesController@index');

});

Now that our database is all set up with sample data, we can finally get to testing.

Responses Without Cache

Let us see what our conventional controller action methods look like without caching and how long it takes to process the response. In the index() method, return a resource of articles:

public function index() {
    $articles = Articles::all();
    return response()->json($articles);
}

You can now run the app and access the url (http://localhost/api/articles) from Postman or in browser as seen below.

Take note of the time taken to complete this request on a local development server.

Responses with Cache

Let us now try to use caching and see if there will be any significant difference in the time taken to respond with data. Change the index() method to:

 public function index() {
    $articles = Cache::remember('articles', 22*60, function() {
        return Article::all();
    });
    return response()->json($articles);
}

Now we are caching the articles using the remember() method we discussed for 22 hours. Run again and observe the time taken. See my screenshot:

Results & Recommendation

From my standard development PC, the time taken to produce a response when using cache is less compared to when not as seen in the table:

Without Cache

Server Hits Time
1st 4478ms
2nd 4232ms
3rd 2832ms
4th 3428ms
Avg 3742ms

With Cache (File driver)

Server Hits Time
1st 4255ms
2nd 3182ms
3rd 2802ms
4th 3626ms
Avg 3466ms

With Cache (Memcached driver)

Server Hits Time
1st 3626ms
2nd 566ms
3rd 1462ms
4th 1978ms
Avg 1908ms :)

With Cache (Redis driver)

It is required to install predis/predis via composer

Server Hits Time
1st 3549ms
2nd 1612ms
3rd 920ms
4th 575ms
Avg 1664ms :)

Awesome enough right? Two things to note:

  1. The first hits take much more time even when using cache. This is because the cache is still empty during the first hit.
  2. Memcached and Redis are way faster compared to file. It is advised we use an external cache driver when our project is large.

Conclusion

The speed difference might not be so obvious with a file/database driver, but if we use external providers, we will see a lot better performance. It pays to invest in caching.

Chris Nwamba

Passion for instructing computers and understanding its language. Would love to remain a software engineer in my next life.