DylanBaine.com / Browse / Scaling Laravel Sanctum

Scaling Laravel Sanctum

At Sweetwater, we leverage Laravel Sanctum for API authentication in some of our service. In this post, I’d like to share with you what we did to make sure that Laravel Sanctum wouldn’t slow down the millions of requests we handle a day.

The Problem

Laravel Sanctum is really nice in the sense that it provides out of the box bearer token generation and verification (plus a little more) to any Laravel application. The problem this brings to applications that get huge amounts of traffic, is that every request that is guarded by your api middleware will require several calls to the database. When you are servicing hundreds of requests a second, every database query matters.

What queries does Laravel Sanctum run?

When implementing Sanctum, I found that it makes 3 database queries per request.

Here are those 3 queries.

"select * from `personal_access_tokens` where `personal_access_tokens`.`id` = '1' limit 1";

"select * from `users` where `users`.`id` = 1 limit 1";

"update `personal_access_tokens` set `last_used_at` = '2021-04-24 17:28:01', `personal_access_tokens`.`updated_at` = '2021-04-24 17:28:01' where `id` = 1";

The first query is finding the personal access token that is in the incoming request, the second query is to get the “User” that the access token belongs to, and the last query updates the personal_access_tokens table with the current time. There is no inherit problem with these queries, however, when looking to save database queries whenever possible, this looks like a good place for optimization.

The Solution

Let’s look at the first query. Finding the Personal Access Token in the database.

After digging through stack traces, I found that Sanctum is using a static method called findToken() in the Laravel\Sanctum\PersonalAccessToken model. Luckily, we can override this method by telling Sanctum that we would like to use our own model for PersonalAccessToken’s.

We can define the custom PersonalAccessToken model by calling Sanctum::usePersonalAccessTokenModel(MyCustomPersonalAccessToken::class) in a service provider. I put ours in the AuthServiceProvider class. To make it easy, I created the custom PersonalAccessToken model and extended the Laravel\Sanctum\PersonalAccessToken class. Then customized the findToken() method.

Here’s how our custom findToken() method looks:

    public static function findToken($token)
    {
        $token = Cache::remember("PersonalAccessToken::$token", 600, function () use ($token) {
            return parent::findToken($token) ?? '_null_';
        });
        if ($token === '_null_') {
            return null;
        }
        return $token;
    }

As you can see, most of the work is done with the handy Cache::remember method that Laravel gives us. We are also still leveraging Sanctum’s default logic to find a token.

You probably notice that we are returning the string “_null_” in the case that we don’t find a token. This is because the Laravel Cache::remeber() method does not cache null values, and we don’t want to query for existent access tokens more than we have to.

Let’s get rid of the second query. Finding the “User” that the token belongs to.

Luckily, we’ve already done most of the work by creating a custom PersonalAccessToken model. Sanctum is leveraging an Eloquent Relationship to resolve the user by calling $token->tokenable. If you are already familiar with Eloquent, you understand what this is doing…

How I got rid of this query was by adding a custom attribute accessor to our custom PersonalAccessToken class.

 public function getTokenableAttribute()
    {
        return Cache::remember("PersonalAccessToken::{$this->id}::tokenable", 600, function () {
            return parent::tokenable()->first();
        });
    }

As you can see, we are leveraging Cache::remember and still using the parent Sanctum PersonalAccessToken class’s logic when resolving the owner of the token.

Finally, let’s squish that last database query. Updating the personal_access_tokens table.

This one is a little more… strange. I came up with the solution by reading the Laravel source code quite a bit over the last couple of years. I’ll start with the code, then go from there.

    public static function boot()
    {
        parent::boot();
        // When updating, cancel normal update and manually update
        // the table asynchronously every hour.
        static::updating(function (self $personalAccessToken) {
            try {
                Cache::remember("PersonalAccessToken::lastUsgeUpdate", 3600, function () use ($personalAccessToken) {
                    dispatch(new UpdatePersonalAccessToken($personalAccessToken, $personalAccessToken->getDirty()));
                    return now();
                });
            } catch (Exception $e) {
                Log::critical($e->getMessage());
            }
            return false;
        });
    }

This boot() method is called every time an Eloquent Model is made.

The static::updating() method allows us to run a function every time an eloquent model is being saved, allowing us to customize what happens when we update that model. In this case, we are dispatching a job to run in the background no more than once an hour. Again, I enforced this hour lock with the handy-dany Cache::remember method.

Here’s the code that is running the UpdatePersonalAccessToken job.

    public function handle()
    {
        DB::table($this->personalAccessToken->getTable())
            ->where('id', $this->personalAccessToken->id)
            ->update($this->newAttributes);
    }

Pretty simple. An important thing to note is that we are leveraging the Laravel Query Builder to update the token instead of the PersonalAccessToken model. This is so that we don’t trigger a never-ending loop of dispatching update jobs by triggering the static::updating() logic.

I hope this post was informative and helps you scale your Larvel applications!