Improving Laravel Sanctum Personal Access Token Performance

I was recently working on a project using Laravel Sanctum personal access tokens for API authentication.

One odd thing I noticed was that a subset of requests coming in seemed to take 5-10x longer than others. Digging into them, I discovered that on every request, Sanctum was performing a database write to update the last_used_at column on the personal_access_tokens table. Most of my requests were still pretty quick, but as the volume of requests increased, some of these writes were bottlenecking and slowing down a bit.

I’m not the first to notice this, but there isn’t an easy way to directly disable this within Sanctum at this time.

A Quick Solution

I didn’t need that last used information for my project, so to work around it I created custom PersonalAccessToken model that short-circuits the save() method when the only column changed is the last_used_at property.

First I created a new model using the artisan console:

php artisan make:model PersonalAccessTokenCode language: Bash (bash)

Then inside that model, I used this code:

<?php

namespace App;

use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{
    /**
     * Limit saving of PersonalAccessToken records
     *
     * We only want to actually save when there is something other than
     * the last_used_at column that has changed. It prevents extra DB writes
     * since we aren't going to use that column for anything.
     *
     * @param  array  $options
     * @return bool
     */
    public function save(array $options = [])
    {
        $changes = $this->getDirty();
        // Check for 2 changed values because one is always the updated_at column
        if (! array_key_exists('last_used_at', $changes) || count($changes) > 2) {
            parent::save();
        }
        return false;
    }
}
Code language: PHP (php)

This custom model extends the model included in Sanctum and only has one method, save().

It gets whatever values have been edited and then checks to see if it’s trying to save anything other than last_used_at. If so, it will call the save() method on the parent model from Sanctum. Otherwise it just terminates by returning false to indicate nothing was saved.

After the custom model is created, we need to tell Sanctum to use our version instead of the default. You can do that in the boot() method of the Providers/AppServiceProvider.php class. Sanctum has a usePersonalAccessTokenModel() static method that we can use to assign the model to be used for the tokens.

It will look something like this. This is the entire file if you haven’t added anything else, but the highlighted part is the relevant portion.

<?php

namespace App\Providers;

use App\PersonalAccessToken;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        // Use our customized personal access token model
        Sanctum::usePersonalAccessTokenModel(
            PersonalAccessToken::class
        );
    }
}

Code language: PHP (php)

If you named your custom model something else, you’ll need to use that instead of PersonalAccessToken::class.

After implementing this, I saw an immediate improvement from eliminating those costly database writes. This would be even more relevant if you had a more complex database setup, such as using replication and separating reads/writes.

5 Comments

  1. Great post! I’m monitoring my queries with telescope and I noticed this update because it is the one that takes the longest. These days we will put a replica server for the readings, but first I will apply your solution.

    Greetings!

    • No, there are 2 columns that get updated each time, last_used_at and updated_at. We should only call save if more than 2 columns are updated.

Comments are closed.