🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)
🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 8 Min Lesezeit
0

The Hidden N+1 in Laravel Authorization (And Why Caching Alone Doesn’t Fix It)

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

TL;DR — In a typical authenticated Laravel app using spatie/laravel-permission, every request that touches roles or permissions pays a fixed cost of *~4 database queries*, even with the permission cache enabled. That cost doesn't scale with the number of hasPermissionTo() calls — it's the same whether you check 1 permission or 100. But it also never goes to zero. Below, I'll show you how to detect it in 5 minutes, why caching alone doesn't fix it, and three concrete paths out.










Eloquent's N+1 has a quiet cousin



Every Laravel developer has had the "wait, why is this page 200 queries?" moment. We learn with(), we install Telescope or Debugbar, and we move on. But there's a second N+1 hiding in plain sight inside almost every authenticated app: the permission check.



It's quieter than the classic N+1 for two reasons:




  1. It doesn't scale with the response payload — so it doesn't get worse as your data grows.

  2. The most popular permissions package (spatie/laravel-permission) already caches "the permission registry," which gives developers the comforting feeling that authorization is solved.



It's not — at least not the way most of us assume. The cache helps. But there's still a fixed tax on every authenticated request, and on a high-traffic API it adds up to real money.



Let's look.









A 30-line reproduction



Spin up a fresh Laravel 11 app, install spatie/laravel-permission, and add this controller:




CODE
// app/Http/Controllers/DashboardController.php
public function index(Request $request)
{
$user = $request->user();

$can = [
'view' => $user->can('view dashboard'),
'edit' => $user->can('edit dashboard'),
'export' => $user->can('export dashboard'),
'archive' => $user->can('archive dashboard'),
'isAdmin' => $user->hasRole('admin'),
];

return response()->json($can);
}






Five checks. Nothing exotic. Now wrap it with a tiny query logger:




CODE
// app/Providers/AppServiceProvider.php — boot()
DB::listen(function ($query) {
Log::info($query->sql, ['bindings' => $query->bindings, 'time' => $query->time]);
});






Hit the endpoint authenticated, then check storage/logs/laravel.log. With the cache enabled, you'll see something like:




CODE
select * from "users" where "users"."id" = ? limit 1
select "roles".*, "model_has_roles"."model_id" as "pivot_model_id" ...
select "permissions".*, "model_has_permissions"."model_id" as "pivot_model_id" ...
select "permissions".* from "permissions" inner join "role_has_permissions" ...






Four queries. Every request. Even though you're only checking five things. Even with the cache. Now bump the controller to 50 checks: still 4 queries. Now drop it to 1 check: still 4 queries. Welcome to the fixed tax.









Why the cache doesn't make it zero



The Spatie cache is doing real work — but it's caching the wrong thing for this scenario. Specifically, it caches the global registry: the list of permissions, the list of roles, and which permissions belong to which role. That's why your hasPermissionTo('edit dashboard') call doesn't trigger a query to permissions every time — the lookup happens in memory.



What is not cached, by default, is the join between this user and their roles/permissions. The first time you ask "does this user have role X?" Eloquent lazy-loads:





  1. users — the user itself.


  2. roles via model_has_roles — what roles this user has.


  3. permissions via model_has_permissions — direct permissions on this user.


  4. permissions via roles — permissions inherited via roles.



That's the 4-query floor. It's the same shape whether the user has 1 role or 50. And once those are hydrated, subsequent checks in the same request are free.



The trick is that "the same request" is the only place where caching helps. Two seconds later, on a different request, the cycle restarts. Four queries again. For every authenticated user. For every request.



If your app serves 50 req/s of authenticated traffic, that's 200 permission-related queries per second that you're paying as table stakes.









Finding your own number in 5 minutes



Don't trust me. Measure your app. The cheapest path:




CODE
// AppServiceProvider::boot()
if (app()->environment('local')) {
DB::listen(function ($q) {
if (str_contains($q->sql, 'role') || str_contains($q->sql, 'permission')) {
logger()->channel('single')->info('[AUTHZ]', [
'sql' => $q->sql, 'time_ms' => $q->time,
]);
}
});
}






Hit five different authenticated endpoints. Count entries per request. If the number is consistent and >0, that's your N+1. If it scales with response data, you have a different N+1 — and you should fix that first.



For a more graphical picture, install around. The core idea:




  • For each user, store their resolved permissions and roles as Redis SETs:




CODE
  user:42:permissions  →  {edit dashboard, view dashboard, ...}
user:42:roles → {admin, editor}







  • A permission check becomes a single SISMEMBERO(1), no array deserialization, no scan.

  • Cache invalidation is surgical: when a role's permissions change, only the affected users get rewarmed. No drop-all, no thundering herd.

  • An in-memory per-request layer sits in front of Redis, so repeated checks in the same request don't even hit the network.




CODE
// With the package installed and the trait on User:
$user->hasPermissionTo('edit dashboard'); // SISMEMBER under the hood
$user->hasRole('admin'); // SISMEMBER






The trade-off is honest: you need Redis. If Redis isn't already in your stack, this adds an infra dependency. If it is — for sessions, queues, cache, broadcasting — you're not adding a new system, you're using it more.









The numbers, side by side



I built a ships with a one-command migration from Spatie (php artisan permissions-redis:migrate-from-spatie).

  • The ·

    Vollständiger Original-Bericht
    Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
    ↗ Original-Artikel auf dev.to lesen
  • Wie bewertest du diesen Beitrag?
    1 Klick Feedback
    Teilen mit Netzwerk & Team:

    Community-Analysen & Experten-Meinungen 0

    Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
    Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
    Community Pulse: Relevanz-Einschätzung
    1 Klick Experten-Votum
    🔴 Akute Relevanz 0%
    🟡 In Evaluierung 0%
    🟢 Keine Auswirkung 0%
    Spannende Innovation 0%
    Verwandte Story-Cluster & Quellen (Vektor-KI)
    Port 8095 Engine
    1 Quelle
    Modify Windows Support Phone Number with PowerShell
    1 Quelle
    Die Zukunft des Einkaufens: Warum wir ein neues Kapitel aufschlagen (und wie du es mitschreiben kannst)
    1 Quelle
    ZDE Podcast 251: Wie sieht digitales Instore Marketing 2026 aus, Amit Chatterjee?
    Ähnliche Beiträge
    🔍 Verwandte News

    Auch interessante Nachrichten The Hidden N+1 in Laravel Authorization (And Why Caching Alone Doesn’t Fix It)

    Thematisch verwandte Begriffe: Hidden, Laravel, Authorization, Caching · 6 Treffer

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...