Here's the most important optimization in this chapter: cache tokens in memory instead of querying the database every request.
Let's do the math. Start with a realistic baseline: a service-to-service API handling 60 requests per minute. Each request validates a Bearer token. Without caching:
- 60 requests/min = 60 database queries/min
- Call it 3,600 queries per hour in normal operation
- Multiply across multiple staging environments and you're easily 5-10k queries per hour
Not catastrophic, but also not free. Each query hits the database, uses a connection, adds latency to the response.
But here's where it matters: this is a service-to-service API. Unlike user-facing APIs that serve millions of occasional requests, service-to-service APIs handle fewer distinct callers making repeated requests. A deployment system might poll your API thousands of times per hour. A billing service runs batch operations against your API constantly. Organization integrations hit your endpoints repeatedly throughout the day.
Scale happens fast. That baseline 60 requests/min can easily jump to 600, then 1000+ as more partners integrate. When you reach 1000 requests per minute, the math changes dramatically:
Without caching at 1000 requests/min:
- 1000 requests/min = 1000 database queries/min = 60,000 queries per hour
- Your database connection pool gets stressed
- Each query adds 5-10ms of latency
- Every response is slightly slower
- Under load spikes, database becomes the bottleneck
With caching at 1000 requests/min:
- 1000 requests/min against cache = instant hits (< 1ms)
- Only new tokens cause database queries
- In steady state with repeated callers: roughly 50-100 database queries per hour
- 99% reduction in database load
- Responses stay fast even under 10x traffic spikes
That is where caching becomes critical. You go from a scalability problem to a non-problem. Your database can handle 1000 requests per minute when you're querying it once per hour instead of 60,000 times.
However, earlier in this guide, we emphasized avoiding premature optimization and not solving problems you don't have yet. Token caching is the exception, and here's why. Adding caching costs nothing, a few lines of code in a trait, reused in two places. It adds zero complexity to your system. But it unlocks 10x scalability for free. Most importantly, scaling from 60 requests/min to 1000 requires zero code changes. You deploy the same application. You add more servers, increase your cache TTL, adjust database pool sizes. Infrastructure scaling, not code changes. That's the hallmark of correct architecture: you built it right, so growth is a deployment problem, not a refactoring problem. This is different from speculative architecture built for a problem you might never encounter. Caching is proven, simple, and essential from day one in any authentication system. Build it now and thank yourself later.
Here's how it works:
// app/Concerns/CachesBearerTokens.php
trait CachesBearerTokens
{
private function rememberBearerToken(string $token): ?Bearer
{
// Check cache first
$cached = Cache::get("bearer:{$token}");
if ($cached instanceof Bearer) {
return $cached; // Cache hit, return immediately
}
// Cache miss, query the database
$bearer = Bearer::where('token', $token)->first();
if ($bearer === null) {
return null; // Token doesn't exist, no point caching
}
// Calculate how long to cache based on expiration
$ttl = $this->calculateBearerCacheTTL($bearer);
Cache::put("bearer:{$token}", $bearer, $ttl);
return $bearer;
}
}
private function calculateBearerCacheTTL(Bearer $bearer): int
{
// If token never expires, cache for 1 hour
if ($bearer->expires_at === null) {
return 3600;
}
// Cache until seconds remaining before expiration
// But never less than 60 seconds (avoid thrashing cache)
return (int) max(60, $bearer->expires_at->diffInSeconds(now()));
}
The TTL calculation is clever. Tokens that expire in 10 seconds only get cached for 10 seconds. Why? Because caching an expired token is useless. You'd hit the cache, get the token, then discover it's expired, and have wasted the cache lookup. But tokens that expire in 1 month get cached for 1 month. Why? Because they're stable. They won't change. You're not lying to the client by returning cached data from last month.
The beutiful part is this: anyone who needs to the "rememberBearerToken" can call this trait. any middleware. The LicenseServiceProvider uses it. Both hit the same cache key. So:
Request arrives
↓
Middleware calls rememberBearerToken()
→ First request to the token: database query, cache for TTL, return
→ Second request in same hour: cache hit, no database query
↓
LicenseServiceProvider calls rememberBearerToken()
→ Same cache hit from middleware, no additional query
↓
Controller executes with token already validated and configured
One database query, two validations, infinite scalability.