<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sebastian Cabarcas</title>
    <description>The latest articles on Forem by Sebastian Cabarcas (@scabarcas).</description>
    <link>https://forem.com/scabarcas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834190%2F1dab13e4-3930-4f60-885a-9a3c8e6bab78.png</url>
      <title>Forem: Sebastian Cabarcas</title>
      <link>https://forem.com/scabarcas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/scabarcas"/>
    <language>en</language>
    <item>
      <title>laravel-permissions-redis v4.0.0 is now stable</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Thu, 14 May 2026 21:31:54 +0000</pubDate>
      <link>https://forem.com/scabarcas/laravel-permissions-redis-v400-is-now-stable-1fjh</link>
      <guid>https://forem.com/scabarcas/laravel-permissions-redis-v400-is-now-stable-1fjh</guid>
      <description>&lt;p&gt;After three weeks of internal production validation without surfacing new bugs, &lt;code&gt;scabarcas/laravel-permissions-redis&lt;/code&gt; ships its first stable 4.x release today. The codebase is identical to &lt;code&gt;v4.0.0-beta.2&lt;/code&gt;. What changed is the commitment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "no code changes" is the point
&lt;/h2&gt;

&lt;p&gt;Stable doesn't mean "we added features." Stable means "we stop changing this." For three weeks I ran the beta in my own production workloads, expecting to find something — an edge case, a race condition, a missed invalidation. Nothing surfaced. The Redis contract test suite stayed green, the in-memory test fake matched the real Redis implementation, and the resolver behaved consistently across queue workers, Octane lifecycles, and multi-user-model setups.&lt;/p&gt;

&lt;p&gt;When a beta runs long enough to stop producing new bug reports, the right thing is to stop labeling it beta. Three weeks felt like enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get if you're coming from v3 or earlier
&lt;/h2&gt;

&lt;p&gt;The 4.x line has been collecting features since late March. If you haven't followed the beta releases, the short list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permission group metadata in Redis&lt;/strong&gt; — group names survive the cache layer, so &lt;code&gt;PermissionResolver::getAllPermissions()&lt;/code&gt; returns enriched DTOs without falling back to the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-user-model support&lt;/strong&gt; — &lt;code&gt;user_model&lt;/code&gt; config accepts an array, &lt;code&gt;Gate::before&lt;/code&gt; iterates over every configured type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue-backed warming&lt;/strong&gt; — &lt;code&gt;WarmUserCacheJob&lt;/code&gt; plus &lt;code&gt;--queue&lt;/code&gt; flag on warm commands. No more synchronous bulk warming under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard parameter on every Blade directive&lt;/strong&gt; — &lt;code&gt;@role('admin', 'api')&lt;/code&gt;, &lt;code&gt;@permission('users.read', 'api')&lt;/code&gt;, all six directives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LRU eviction + warm cooldown&lt;/strong&gt; in the in-memory resolver caches. Long-running workers no longer grow unbounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic permission groups + Redis SCAN/HSET fixes&lt;/strong&gt; — every mutation goes through MULTI/EXEC with consistent rollback semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration command from Spatie&lt;/strong&gt; — &lt;code&gt;php artisan permissions-redis:migrate-from-spatie&lt;/code&gt; reads your existing &lt;code&gt;model_has_roles&lt;/code&gt;, &lt;code&gt;model_has_permissions&lt;/code&gt;, and &lt;code&gt;role_has_permissions&lt;/code&gt; tables and warms the equivalent Redis state in one shot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full audit-and-fix list is in the &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark recap
&lt;/h2&gt;

&lt;p&gt;For new readers — the differentiator stays the same:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;spatie p50&lt;/th&gt;
&lt;th&gt;redis p50&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 authorization-heavy request&lt;/td&gt;
&lt;td&gt;13.76 ms&lt;/td&gt;
&lt;td&gt;1.26 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.94x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 iterations&lt;/td&gt;
&lt;td&gt;138.87 ms&lt;/td&gt;
&lt;td&gt;13.01 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.68x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 iterations&lt;/td&gt;
&lt;td&gt;696.73 ms&lt;/td&gt;
&lt;td&gt;63.79 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.92x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Methodology: 5 warm-up runs + 30 measurement runs per scenario, GC reset between runs, predis client, SQLite + local Redis on Apple Silicon. Reproducible with one Docker command from the &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;benchmark repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The 10x is not from a smarter algorithm — it's from doing less work. Spatie hydrates the user-to-roles-to-permissions chain on every request via Eloquent (4 DB queries per authorization-heavy endpoint). This package keeps that chain in Redis SETs and checks membership with &lt;code&gt;SISMEMBER&lt;/code&gt; (O(1)). The DB query left is the &lt;code&gt;users&lt;/code&gt; lookup itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semver promise from here
&lt;/h2&gt;

&lt;p&gt;Breaking changes will only ship in &lt;code&gt;v5.0.0&lt;/code&gt;. The 4.x line gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Non-breaking features&lt;/li&gt;
&lt;li&gt;Bug fixes&lt;/li&gt;
&lt;li&gt;Documentation improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your &lt;code&gt;composer.json&lt;/code&gt; constraint &lt;code&gt;^4.0&lt;/code&gt; is safe to commit. If your test suite passes against &lt;code&gt;v4.0.0&lt;/code&gt;, it will keep passing across every 4.x release.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this package fits
&lt;/h2&gt;

&lt;p&gt;It's not a Spatie replacement for every Laravel app. Use this when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You make many authorization checks per request (&lt;code&gt;hasPermissionTo&lt;/code&gt;, &lt;code&gt;hasRole&lt;/code&gt;, Blade directives, &lt;code&gt;Gate::allows&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Redis is already a production dependency (sessions, queues, cache)&lt;/li&gt;
&lt;li&gt;The 4 DB queries per request from Spatie's relation hydration show up in your traces&lt;/li&gt;
&lt;li&gt;You can absorb the eventual-consistency window of cache invalidation (events trigger rewarming within milliseconds, but it's not strictly synchronous)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spatie remains the right default for the 95% of apps where authorization isn't on the hot path. Don't switch unless the latency math actually moves your numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require scabarcas/laravel-permissions-redis:^4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/scabarcas17/laravel-permissions-redis#readme" rel="noopener noreferrer"&gt;Full README with setup steps&lt;/a&gt; · &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis#migrating-from-spatie" rel="noopener noreferrer"&gt;Migrate from Spatie in one command&lt;/a&gt; · &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;Benchmark methodology&lt;/a&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>laravel</category>
      <category>performance</category>
      <category>permissions</category>
    </item>
    <item>
      <title>A Redis-first alternative to spatie/laravel-permission, benchmarked</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Tue, 12 May 2026 16:09:23 +0000</pubDate>
      <link>https://forem.com/scabarcas/a-redis-first-alternative-to-spatielaravel-permission-benchmarked-9nn</link>
      <guid>https://forem.com/scabarcas/a-redis-first-alternative-to-spatielaravel-permission-benchmarked-9nn</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built a Redis-first roles &amp;amp; permissions package for Laravel called &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;&lt;code&gt;scabarcas/laravel-permissions-redis&lt;/code&gt;&lt;/a&gt; — Spatie-compatible API (&lt;code&gt;hasRole&lt;/code&gt;, &lt;code&gt;hasPermissionTo&lt;/code&gt;, Blade directives, middleware), but the user→roles→permissions mapping lives in Redis SETs instead of being hydrated from the DB on every request.&lt;/p&gt;

&lt;p&gt;Benchmark vs &lt;code&gt;spatie/laravel-permission ^7.2&lt;/code&gt;, with 5 warm-up + 30 measurement runs per scenario, GC reset between runs, predis client, SQLite + local Redis on Apple Silicon:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Spatie p50&lt;/th&gt;
&lt;th&gt;Redis p50&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;th&gt;DB queries reduced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 authorization-heavy request&lt;/td&gt;
&lt;td&gt;13.76 ms&lt;/td&gt;
&lt;td&gt;1.26 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.94x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 → 1 (75%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 iterations&lt;/td&gt;
&lt;td&gt;138.87 ms&lt;/td&gt;
&lt;td&gt;13.01 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.68x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40 → 10 (75%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 iterations&lt;/td&gt;
&lt;td&gt;696.73 ms&lt;/td&gt;
&lt;td&gt;63.79 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.92x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200 → 50 (75%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speedup is consistent — Redis lookups are near-constant time; Spatie's per-request relation hydration scales linearly. The whole bench repo is public if you want to reproduce: &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;&lt;code&gt;laravel-permissions-redis-benchmark&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Rest of the post is the &lt;em&gt;why&lt;/em&gt; and the &lt;em&gt;when not to use it&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with Spatie isn't the cache — it's relation hydration
&lt;/h2&gt;

&lt;p&gt;If you've used &lt;code&gt;spatie/laravel-permission&lt;/code&gt; in a high-traffic app, you've probably noticed that authorization checks cost more than they look. Here's a query log from a single request that calls &lt;code&gt;hasPermissionTo()&lt;/code&gt; 27 times, &lt;code&gt;hasRole()&lt;/code&gt; 4 times, plus &lt;code&gt;getAllPermissions()&lt;/code&gt; and &lt;code&gt;getRoleNames()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. User lookup&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="c1"&gt;-- 2. Roles via pivot&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="nv"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"model_has_roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_id"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"pivot_model_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
       &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;"roles"&lt;/span&gt; &lt;span class="k"&gt;inner&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="nv"&gt;"model_has_roles"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"model_has_roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"role_id"&lt;/span&gt;
       &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nv"&gt;"model_has_roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;"model_has_roles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_type"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'App&lt;/span&gt;&lt;span class="se"&gt;\M&lt;/span&gt;&lt;span class="s1"&gt;odels&lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s1"&gt;ser'&lt;/span&gt;
&lt;span class="c1"&gt;-- 3. Direct permissions via pivot&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"model_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_id"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"pivot_model_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
       &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt; &lt;span class="k"&gt;inner&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="nv"&gt;"model_has_permissions"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"model_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"permission_id"&lt;/span&gt;
       &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nv"&gt;"model_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;"model_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"model_type"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'App&lt;/span&gt;&lt;span class="se"&gt;\M&lt;/span&gt;&lt;span class="s1"&gt;odels&lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s1"&gt;ser'&lt;/span&gt;
&lt;span class="c1"&gt;-- 4. Permissions via roles&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"role_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"role_id"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"pivot_role_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
       &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt; &lt;span class="k"&gt;inner&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="nv"&gt;"role_has_permissions"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"role_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"permission_id"&lt;/span&gt;
       &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nv"&gt;"role_has_permissions"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"role_id"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four queries. Every request that authorizes anything. Doing 27 permission checks doesn't multiply that — it's still 4 queries, because Spatie loads the user's relations &lt;em&gt;once&lt;/em&gt; per &lt;code&gt;User::find()&lt;/code&gt; and then runs the membership checks in PHP memory.&lt;/p&gt;

&lt;p&gt;But here's the part that surprised me when I first profiled it: &lt;strong&gt;Spatie's permission cache doesn't help with those 4 queries.&lt;/strong&gt; That cache stores the &lt;em&gt;global&lt;/em&gt; permission and role registry — "what permissions exist in the system" — using &lt;code&gt;cache.permissions.cache&lt;/code&gt; via the Laravel cache facade. The per-user pivot relations (&lt;code&gt;model_has_roles&lt;/code&gt;, &lt;code&gt;model_has_permissions&lt;/code&gt;, &lt;code&gt;role_has_permissions&lt;/code&gt;) are loaded by Eloquent every time you call &lt;code&gt;User::find()&lt;/code&gt; followed by a permission check. The global cache only saves you from re-reading the &lt;code&gt;permissions&lt;/code&gt; and &lt;code&gt;roles&lt;/code&gt; tables themselves.&lt;/p&gt;

&lt;p&gt;This is fine for low-traffic apps. It's a tax you stop noticing once you're past the database-side bottleneck.&lt;/p&gt;

&lt;p&gt;It's annoying once you start having authorization in every middleware, every gate, every Blade directive on dashboards, every API endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I went Redis-first instead of "Spatie + better cache"
&lt;/h2&gt;

&lt;p&gt;The natural next step is to cache the &lt;em&gt;user's resolved permissions&lt;/em&gt;, not just the global registry. You can do that with Spatie by writing a custom decorator that caches &lt;code&gt;getAllPermissions()&lt;/code&gt; per user — and people have done this. Two problems show up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invalidation gets sharp.&lt;/strong&gt; When you grant a user a permission, you need to clear that user's cache. When you change a role's permissions, you need to clear &lt;em&gt;every user that has that role&lt;/em&gt;. When you delete a permission, you need a broader sweep. Spatie's API is built around &lt;code&gt;forgetCachedPermissions()&lt;/code&gt; which nukes everything — fine, but every assignment change forces every active user back into the 4-query path on their next request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The data structure is wrong for the operation.&lt;/strong&gt; Even if you cache per-user permissions as a JSON array, checking &lt;code&gt;hasPermissionTo('posts.edit')&lt;/code&gt; is an &lt;code&gt;in_array()&lt;/code&gt; scan over the deserialized array. Redis SETs do this in O(1) with &lt;code&gt;SISMEMBER&lt;/code&gt;. Once you're already in Redis for the data, you might as well use the right operation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So instead of a Spatie decorator I wrote a separate trait. The wire format in Redis is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;SET&lt;/span&gt;  &lt;span class="n"&gt;permissions&lt;/span&gt;:&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="n"&gt;permissions&lt;/span&gt;   {&lt;span class="s2"&gt;"posts.view"&lt;/span&gt;, &lt;span class="s2"&gt;"posts.edit"&lt;/span&gt;, &lt;span class="s2"&gt;"users.view"&lt;/span&gt;, ...}
&lt;span class="n"&gt;SET&lt;/span&gt;  &lt;span class="n"&gt;permissions&lt;/span&gt;:&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="m"&gt;1&lt;/span&gt;:&lt;span class="n"&gt;roles&lt;/span&gt;         {&lt;span class="s2"&gt;"admin"&lt;/span&gt;, &lt;span class="s2"&gt;"editor"&lt;/span&gt;}
&lt;span class="n"&gt;HASH&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt;:&lt;span class="n"&gt;role&lt;/span&gt;:&lt;span class="n"&gt;editor&lt;/span&gt;          {&lt;span class="n"&gt;permissions&lt;/span&gt;: [&lt;span class="s2"&gt;"..."&lt;/span&gt;], ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hasPermissionTo($perm)&lt;/code&gt; becomes &lt;code&gt;SISMEMBER permissions:user:{id}:permissions {$perm}&lt;/code&gt;. &lt;code&gt;hasRole($role)&lt;/code&gt; is the same against &lt;code&gt;:roles&lt;/code&gt;. Wildcard checks (&lt;code&gt;posts.*&lt;/code&gt;) use &lt;code&gt;SMEMBERS&lt;/code&gt; + &lt;code&gt;fnmatch()&lt;/code&gt; in PHP, which is still constant queries.&lt;/p&gt;

&lt;p&gt;Cache warm happens on login (or explicitly via &lt;code&gt;AuthorizationCacheManager::warmUser($userId)&lt;/code&gt;). After that, every authorization check is a Redis round-trip — no DB queries except the user lookup itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, with methodology
&lt;/h2&gt;

&lt;p&gt;Here's the bench harness in one screen (&lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;source&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BenchmarkRunner&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$iterations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$warmUpRuns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$measurementRuns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;strategies&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Flush Spatie cache once so both strategies start from the same baseline&lt;/span&gt;
            &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PermissionRegistrar&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forgetCachedPermissions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$warmUpRuns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$iterations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$measurementRuns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;flushQueryLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nb"&gt;gc_collect_cycles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$iterations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// Aggregate into p50, p95, p99, mean, stddev&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each "iteration" performs 27 &lt;code&gt;hasPermissionTo()&lt;/code&gt; checks + 4 &lt;code&gt;hasRole()&lt;/code&gt; checks + &lt;code&gt;getAllPermissions()&lt;/code&gt; + &lt;code&gt;getRoleNames()&lt;/code&gt;. The bench tests &lt;code&gt;iterations=1&lt;/code&gt;, &lt;code&gt;iterations=10&lt;/code&gt;, and &lt;code&gt;iterations=50&lt;/code&gt; to see how the cost scales when authorization is called more than once per request (typical for views that gate multiple sections).&lt;/p&gt;

&lt;p&gt;Run yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/scabarcas17/laravel-permissions-redis-benchmark
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-permissions-redis-benchmark
composer &lt;span class="nb"&gt;install
&lt;/span&gt;php artisan migrate:fresh &lt;span class="nt"&gt;--seed&lt;/span&gt; &lt;span class="nt"&gt;--seeder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;BenchmarkSeeder
php artisan bench:markdown &lt;span class="nt"&gt;--warm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="nt"&gt;--runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full percentile breakdown (also in the bench README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### 10 Iterations (27 permission checks + 4 role checks + 2 collection calls)&lt;/span&gt;

| Metric            | Spatie         | Redis         | Delta            |
|-------------------|----------------|---------------|------------------|
| DB Queries        | 40             | 10            | 75% fewer        |
| Median (p50)      | 138.87 ms      | 13.01 ms      | 10.68x faster    |
| p95               | 140.13 ms      | 13.78 ms      | —                |
| p99               | 159.27 ms      | 13.87 ms      | —                |
| Mean ± StdDev     | 139.42 ± 3.86  | 13.11 ± 0.45  | —                |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few honest notes about these numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steady-state, not cold-start.&lt;/strong&gt; Warm-up runs amortize app bootstrap, Spatie's global registry cache load, Redis connection setup. We're measuring what users see in production, not the first request after a deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite + local Redis.&lt;/strong&gt; Absolute ms will be different on MySQL or Postgres or remote Redis. The &lt;em&gt;ratio&lt;/em&gt; (~10x median) should hold; the constants won't. The bench is reproducible on your hardware in under 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single user.&lt;/strong&gt; No concurrent load. The bench doesn't simulate hundreds of simultaneous requests competing for Redis connections — that's a separate exercise (k6, wrk).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doesn't cover v4-specific features yet.&lt;/strong&gt; Wildcard permissions, group invalidation, the batch role-check API (&lt;code&gt;userHasAnyRole&lt;/code&gt;, &lt;code&gt;userHasAllRoles&lt;/code&gt;) aren't in the harness. They'd push the delta wider, not narrower, but the current numbers stop at the basic API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 5-line setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require scabarcas/laravel-permissions-redis
php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;permissions-redis-config
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on your User model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Scabarcas\LaravelPermissionsRedis\Traits\HasRedisPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasRedisPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional: in your &lt;code&gt;AuthServiceProvider&lt;/code&gt; or a listener, warm on login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Login&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Login&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthorizationCacheManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warmUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Same &lt;code&gt;hasRole&lt;/code&gt;, &lt;code&gt;hasPermissionTo&lt;/code&gt;, &lt;code&gt;assignRole&lt;/code&gt;, &lt;code&gt;givePermissionTo&lt;/code&gt;, &lt;code&gt;@role&lt;/code&gt;, &lt;code&gt;@can&lt;/code&gt;, &lt;code&gt;role:&lt;/code&gt; middleware as Spatie. The migration adds the same &lt;code&gt;permissions&lt;/code&gt;, &lt;code&gt;roles&lt;/code&gt;, &lt;code&gt;model_has_permissions&lt;/code&gt;, &lt;code&gt;model_has_roles&lt;/code&gt;, &lt;code&gt;role_has_permissions&lt;/code&gt; tables Spatie uses, so a migration &lt;em&gt;from&lt;/em&gt; Spatie is essentially "swap the trait, run &lt;code&gt;warmAll&lt;/code&gt;".&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should NOT use this
&lt;/h2&gt;

&lt;p&gt;The whole pitch falls apart in a couple of cases. Be honest with yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You don't run Redis.&lt;/strong&gt; Adding Redis as a hard dependency just for permissions is overkill if you're on a single-server SQLite setup with 10 daily users. Use Spatie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your authorization is shallow.&lt;/strong&gt; If you do &lt;code&gt;@can('admin')&lt;/code&gt; twice per page and ship 1000 requests/day, the absolute ms savings are tiny and the migration cost isn't worth it. Use Spatie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need a cache-driver-agnostic abstraction.&lt;/strong&gt; This package binds you to Redis. If "we might swap Redis for Memcached / DynamoDB / database cache" is in your near-term plan, this isn't the right call — use Spatie + a custom decorator instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're on Laravel &amp;lt;11.&lt;/strong&gt; This package targets Laravel 11/12/13 and PHP 8.3+. Spatie supports older versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package is honest about all of this in its README — the goal isn't to "replace Spatie" but to give you a clearly different choice when authorization throughput actually matters in your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Package: &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;&lt;code&gt;scabarcas/laravel-permissions-redis&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;composer require scabarcas/laravel-permissions-redis&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Benchmark harness: &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;&lt;code&gt;laravel-permissions-redis-benchmark&lt;/code&gt;&lt;/a&gt; — clone, run, see your own numbers&lt;/li&gt;
&lt;li&gt;Discussions: open on the package repo if you want to compare design notes, ask about migration from Spatie, or report edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback welcome — especially from people who've already written custom Spatie cache decorators or who'd benchmark this against a different workload.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>redis</category>
      <category>performance</category>
      <category>permissions</category>
    </item>
    <item>
      <title>Redis-backed permissions for high-volume Laravel apps: v4.0.0-beta.1</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Thu, 23 Apr 2026 15:36:01 +0000</pubDate>
      <link>https://forem.com/scabarcas/redis-backed-permissions-for-high-volume-laravel-apps-v400-beta1-dk3</link>
      <guid>https://forem.com/scabarcas/redis-backed-permissions-for-high-volume-laravel-apps-v400-beta1-dk3</guid>
      <description>&lt;p&gt;I just shipped &lt;code&gt;v4.0.0-beta.1&lt;/code&gt; of &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;laravel-permissions-redis&lt;/a&gt;, and I want to share both what's in it and the specific problem it exists to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Spatie's &lt;code&gt;laravel-permission&lt;/code&gt; is the de-facto permissions package in the Laravel ecosystem and it's great. But it was designed for the common case: users have roles, roles have permissions, you check them a handful of times per request, you hit the DB, everyone's happy.&lt;/p&gt;

&lt;p&gt;What happens when you scale that model?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An admin panel rendering 200+ ACL-gated widgets per page&lt;/li&gt;
&lt;li&gt;An API gateway fanning out to 30 microservices, each check needing authorization context&lt;/li&gt;
&lt;li&gt;A reporting dashboard pulling user-filtered data with permission checks on every field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, each permission check is a DB roundtrip (even if cached at query level), and your p99 latency is now dominated by authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this package does
&lt;/h2&gt;

&lt;p&gt;Moves the entire read path to Redis.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Roles and permissions are denormalized into Redis SETs: &lt;code&gt;user:{id}:permissions&lt;/code&gt;, &lt;code&gt;user:{id}:roles&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Permission checks become &lt;code&gt;SISMEMBER&lt;/code&gt; — ~0.1ms vs a DB roundtrip of 5-20ms&lt;/li&gt;
&lt;li&gt;Writes (assign/revoke) update the DB, fire events, and rewarm the Redis cache&lt;/li&gt;
&lt;li&gt;Cache invalidation is event-driven and automatic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: Redis dependency, cache warming overhead at user login, slightly more complex write path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in v4.0.0-beta.1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Permission group metadata
&lt;/h3&gt;

&lt;p&gt;Previously &lt;code&gt;PermissionDTO::group&lt;/code&gt; was always &lt;code&gt;null&lt;/code&gt; because Redis didn't store group data. Now there's a Redis hash (&lt;code&gt;permission_groups&lt;/code&gt;) that maps &lt;code&gt;{guard}|{name}&lt;/code&gt; → &lt;code&gt;group&lt;/code&gt;. &lt;code&gt;getAllPermissions()&lt;/code&gt; returns properly enriched DTOs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Role-level permission checks
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Role::hasPermission('posts.create')&lt;/code&gt; — direct SISMEMBER on the role's permission set. Useful when you want to know "does the admin role have X?" without loading a user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue-backed cache warming
&lt;/h3&gt;

&lt;p&gt;Warm commands now accept &lt;code&gt;--queue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan permissions-redis:warm &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default
php artisan permissions-redis:warm-user 42 &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dispatches &lt;code&gt;WarmAllCacheJob&lt;/code&gt; / &lt;code&gt;WarmUserCacheJob&lt;/code&gt; instead of running sync. Useful when warming millions of users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-user-models
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;user_model&lt;/code&gt; as an array in config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'user_model'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Admin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both types are warmed, both are covered by the &lt;code&gt;Gate::before&lt;/code&gt; super-admin callback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blade directive guard override
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@role('admin', 'api')
    ...
@endrole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second argument is the guard name. All six directives (&lt;code&gt;@role&lt;/code&gt;, &lt;code&gt;@hasanyrole&lt;/code&gt;, &lt;code&gt;@hasallroles&lt;/code&gt;, &lt;code&gt;@permission&lt;/code&gt;, &lt;code&gt;@hasanypermission&lt;/code&gt;, &lt;code&gt;@hasallpermissions&lt;/code&gt;) accept it.&lt;/p&gt;

&lt;h3&gt;
  
  
  UUID/ULID role IDs
&lt;/h3&gt;

&lt;p&gt;If your Role model uses non-integer primary keys, v4 handles it. &lt;code&gt;PermissionRepositoryInterface&lt;/code&gt; now types role IDs as &lt;code&gt;int|string&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defensive additions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LRU eviction in the in-memory resolver cache&lt;/strong&gt; — prevents unbounded memory growth in long-running workers (queue workers, Octane). Default limit: 1000 users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm cooldown&lt;/strong&gt; — if Redis cache creation keeps failing, the resolver stops hammering the DB with warm attempts (default: 1 second cooldown per user).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TransactionFailedException&lt;/code&gt;&lt;/strong&gt; — Redis &lt;code&gt;EXEC&lt;/code&gt; returning null/false now throws observable exceptions instead of silently dropping writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Breaking change
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PermissionRepositoryInterface&lt;/code&gt; gained three methods for permission group metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setPermissionGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$groups&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPermissionGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$encodedNames&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;deletePermissionGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$encodedName&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a custom implementation of the interface (tenant-aware or otherwise), you must implement these. If you only use the built-in &lt;code&gt;RedisPermissionRepository&lt;/code&gt;, no code changes — just run &lt;code&gt;php artisan permissions-redis:warm --fresh&lt;/code&gt; after upgrade to populate the new hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the beta
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require scabarcas/laravel-permissions-redis:^4.0@beta
php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;permissions-redis-migrations
php artisan migrate
php artisan permissions-redis:warm &lt;span class="nt"&gt;--fresh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I'm looking for
&lt;/h2&gt;

&lt;p&gt;1-2 week beta window before I cut &lt;code&gt;rc.1&lt;/code&gt;. Feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API ergonomics for the new methods&lt;/li&gt;
&lt;li&gt;Clarity of the upgrade guide&lt;/li&gt;
&lt;li&gt;Anything that breaks in your specific setup (tenancy, custom guards, UUID users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/scabarcas17/laravel-permissions-redis/releases/tag/v4.0.0-beta.1" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt; · &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://packagist.org/packages/scabarcas/laravel-permissions-redis" rel="noopener noreferrer"&gt;Packagist&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, let me know how it goes.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>laravel</category>
      <category>php</category>
      <category>performance</category>
    </item>
    <item>
      <title>Benchmarking Laravel Permission Checks: Database vs Redis</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Wed, 01 Apr 2026 13:41:29 +0000</pubDate>
      <link>https://forem.com/scabarcas/benchmarking-laravel-permission-checks-database-vs-redis-3al7</link>
      <guid>https://forem.com/scabarcas/benchmarking-laravel-permission-checks-database-vs-redis-3al7</guid>
      <description>&lt;p&gt;"How much faster is Redis for permission checks?" is a question I get every time I mention &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;laravel-permissions-redis&lt;/a&gt;. The answer depends on your scale, your access patterns, and what you're measuring.&lt;/p&gt;

&lt;p&gt;So I built a &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;benchmark application&lt;/a&gt; to get real numbers. Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP 8.3&lt;/strong&gt; with OPcache enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 12&lt;/strong&gt; with default configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis 7.2&lt;/strong&gt; running locally (same machine, minimal network latency)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8.0&lt;/strong&gt; running locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packages compared:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;spatie/laravel-permission&lt;/code&gt; v6 (database-backed, using Redis as Laravel cache driver)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scabarcas/laravel-permissions-redis&lt;/code&gt; v3 (Redis SETs, dual-layer cache)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  The data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;1 user with 50 permissions assigned via 3 roles&lt;/li&gt;
&lt;li&gt;Permissions structured in groups: &lt;code&gt;posts.*&lt;/code&gt;, &lt;code&gt;users.*&lt;/code&gt;, &lt;code&gt;settings.*&lt;/code&gt;, &lt;code&gt;reports.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both packages configured with their recommended defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What we measure
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database queries&lt;/strong&gt; -- the number of queries hitting MySQL per request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache architecture&lt;/strong&gt; -- how each package stores and retrieves permission data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidation cost&lt;/strong&gt; -- what happens when permissions change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start&lt;/strong&gt; -- first request after cache flush&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm state&lt;/strong&gt; -- steady-state performance&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important note on fairness:&lt;/strong&gt; Spatie is configured with &lt;code&gt;CACHE_DRIVER=redis&lt;/code&gt; to give it the fastest possible cache backend. This comparison is about &lt;em&gt;architecture&lt;/em&gt;, not "database vs Redis" in the trivial sense.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Benchmark 1: Database queries per request
&lt;/h2&gt;

&lt;p&gt;A typical request in our test application performs 33 permission checks (middleware + policy + inline checks). Here's how many database queries each package generates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;spatie/laravel-permission&lt;/th&gt;
&lt;th&gt;laravel-permissions-redis&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 request (cold cache)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 queries&lt;/td&gt;
&lt;td&gt;1 query&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 request (warm cache)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 queries&lt;/td&gt;
&lt;td&gt;0 queries&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10 sequential requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14 queries&lt;/td&gt;
&lt;td&gt;10 queries&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~29%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;50 sequential requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;54 queries&lt;/td&gt;
&lt;td&gt;50 queries&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why the numbers converge
&lt;/h3&gt;

&lt;p&gt;Both packages cache after the first request. The difference on subsequent requests comes from cache invalidation behavior, which we'll cover next.&lt;/p&gt;

&lt;p&gt;The real story isn't in steady-state -- it's in what happens when the cache isn't warm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 2: Cache architecture deep dive
&lt;/h2&gt;

&lt;p&gt;This is where the architectural differences matter most.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Spatie stores permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache key: "spatie.permission.cache"
Value: Serialized PHP array of ALL permissions for ALL users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every &lt;code&gt;hasPermissionTo()&lt;/code&gt; call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the full serialized blob from Redis (via Laravel Cache)&lt;/li&gt;
&lt;li&gt;Deserialize it (&lt;code&gt;unserialize()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Filter permissions for the current user&lt;/li&gt;
&lt;li&gt;Scan the resulting array for the requested permission&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Time complexity per check:&lt;/strong&gt; O(n) where n = total permissions in the system&lt;/p&gt;

&lt;h3&gt;
  
  
  How laravel-permissions-redis stores permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Redis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth:user:42:permissions"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Value:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Redis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"posts.create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"posts.edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"users.view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every &lt;code&gt;hasPermissionTo()&lt;/code&gt; call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check in-memory PHP array (if already resolved this request) -- &lt;strong&gt;zero I/O&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If miss: single &lt;code&gt;SISMEMBER auth:user:42:permissions "posts.create"&lt;/code&gt; command&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Time complexity per check:&lt;/strong&gt; O(1) -- hash table lookup within the Redis SET&lt;/p&gt;

&lt;h3&gt;
  
  
  What this means in practice
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Spatie&lt;/th&gt;
&lt;th&gt;laravel-permissions-redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First check in a request&lt;/td&gt;
&lt;td&gt;Deserialize full cache + scan&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SISMEMBER&lt;/code&gt; (1 Redis call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second check (same request)&lt;/td&gt;
&lt;td&gt;Deserialize full cache + scan&lt;/td&gt;
&lt;td&gt;In-memory array (0 I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10th check (same request)&lt;/td&gt;
&lt;td&gt;Deserialize full cache + scan&lt;/td&gt;
&lt;td&gt;In-memory array (0 I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per check&lt;/td&gt;
&lt;td&gt;Full permission array loaded&lt;/td&gt;
&lt;td&gt;Only user's permission set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With Spatie, if your application has 10,000 permissions across all users, every single &lt;code&gt;hasPermissionTo()&lt;/code&gt; call loads and scans that entire dataset. With Redis SETs, each check only touches the current user's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 3: Cache invalidation
&lt;/h2&gt;

&lt;p&gt;This is the benchmark that convinced me to build the package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario: Admin changes a user's permissions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Spatie's approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When any permission changes:&lt;/span&gt;
&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PermissionRegistrar&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forgetCachedPermissions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// This calls: Cache::forget('spatie.permission.cache');&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;Every user's cache is gone.&lt;/strong&gt; The next request from &lt;em&gt;any&lt;/em&gt; user pays the full cold-start cost.&lt;/p&gt;

&lt;p&gt;With 50,000 active users and a 200 req/sec API, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~200 concurrent cold-start database queries&lt;/li&gt;
&lt;li&gt;Each query rebuilds the full permission cache&lt;/li&gt;
&lt;li&gt;Cache stampede risk if multiple users hit simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;laravel-permissions-redis approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When user 42's permissions change:&lt;/span&gt;
&lt;span class="nv"&gt;$repository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warmUserCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Only rewarms: auth:user:42:permissions and auth:user:42:roles&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;Only user 42's cache is rewarmed.&lt;/strong&gt; Every other user's cache stays untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invalidation cost comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Spatie&lt;/th&gt;
&lt;th&gt;laravel-permissions-redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Users affected&lt;/td&gt;
&lt;td&gt;ALL&lt;/td&gt;
&lt;td&gt;1 (the changed user)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB queries triggered&lt;/td&gt;
&lt;td&gt;1 heavy query (all permissions)&lt;/td&gt;
&lt;td&gt;2 light queries (1 user's roles + permissions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache stampede risk&lt;/td&gt;
&lt;td&gt;High (all users cold)&lt;/td&gt;
&lt;td&gt;None (only 1 user rewarmed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to full recovery&lt;/td&gt;
&lt;td&gt;Depends on traffic&lt;/td&gt;
&lt;td&gt;Instant (proactive rewarm)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Benchmark 4: Cold start and cache warming
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Single user cold start
&lt;/h3&gt;

&lt;p&gt;When a user logs in with no cached data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spatie:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query all permissions in the system&lt;/li&gt;
&lt;li&gt;Serialize and store in cache&lt;/li&gt;
&lt;li&gt;Deserialize and scan on each check&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;laravel-permissions-redis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query this user's roles (1 query)&lt;/li&gt;
&lt;li&gt;Query permissions for those roles (1 query)&lt;/li&gt;
&lt;li&gt;Write Redis SETs: &lt;code&gt;SADD auth:user:42:permissions "posts.create" "posts.edit" ...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All subsequent checks: &lt;code&gt;SISMEMBER&lt;/code&gt; or in-memory&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Bulk cache warming (deploy scenario)
&lt;/h3&gt;

&lt;p&gt;After a deploy, you might want to pre-warm the cache for all users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# laravel-permissions-redis provides this out of the box&lt;/span&gt;
php artisan permissions-redis:warm

&lt;span class="c"&gt;# Spatie has no equivalent -- cache builds lazily on first request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User count&lt;/th&gt;
&lt;th&gt;Warm time (laravel-permissions-redis)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;td&gt;Chunked processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;~15s&lt;/td&gt;
&lt;td&gt;Parallel Redis pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;~120s&lt;/td&gt;
&lt;td&gt;Batched with progress bar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With Spatie, there's no warm command. The cache rebuilds on the first request after deploy, meaning your first 100-1000 users experience a slow response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 5: Memory usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Per-request memory footprint
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Memory per request (50 permissions/user)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spatie (all permissions cached)&lt;/td&gt;
&lt;td&gt;~2-5 MB (full permission array deserialized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;laravel-permissions-redis&lt;/td&gt;
&lt;td&gt;~50-100 KB (only current user's set)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference grows with the total number of permissions in your system. Spatie loads &lt;em&gt;all&lt;/em&gt; permissions regardless of which user is making the request. &lt;code&gt;laravel-permissions-redis&lt;/code&gt; only loads what's relevant to the current user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visualization
&lt;/h2&gt;

&lt;p&gt;Here's how to think about the performance difference at different scales:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permission checks per request
    |
    |  Spatie (warm)      Redis (warm)
    |  ~~~~~~~~~~~~       ~~~~~~~~~~~~
  5 |  Fast enough        Fast
 15 |  Fine               Fast
 30 |  Noticeable          Fast
 50 |  Slow               Fast
100 |  Very slow           Fast
    |
    +----------------------------------------&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;Spatie's performance degrades linearly with the number of checks per request.&lt;/strong&gt; &lt;code&gt;laravel-permissions-redis&lt;/code&gt; stays constant because each check is O(1).&lt;/p&gt;

&lt;h2&gt;
  
  
  When the difference doesn't matter
&lt;/h2&gt;

&lt;p&gt;Let's be honest about when this optimization is irrelevant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&amp;lt; 50 req/sec with &amp;lt; 10 checks/request:&lt;/strong&gt; Both packages are fast enough. Choose based on features, not performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy, rarely-changing permissions:&lt;/strong&gt; If permissions never change, Spatie's cache stays warm indefinitely. The invalidation advantage disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-user CLI or queue jobs:&lt;/strong&gt; No concurrent cache stampede risk. Cold start cost is a one-time hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the difference is critical
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-traffic APIs:&lt;/strong&gt; 200+ req/sec where each request checks 10+ permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant SaaS:&lt;/strong&gt; Thousands of users with different permission sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time applications:&lt;/strong&gt; WebSocket servers or Octane workers with long-lived processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequent permission changes:&lt;/strong&gt; Admin panels where roles/permissions are edited regularly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large permission matrices:&lt;/strong&gt; 100+ permissions per user across multiple roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproducing these benchmarks
&lt;/h2&gt;

&lt;p&gt;The benchmark application is open source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/scabarcas17/laravel-permissions-redis-benchmark
&lt;span class="nb"&gt;cd &lt;/span&gt;laravel-permissions-redis-benchmark
composer &lt;span class="nb"&gt;install
&lt;/span&gt;php artisan migrate &lt;span class="nt"&gt;--seed&lt;/span&gt;
php artisan benchmark:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It generates a side-by-side comparison report with your specific hardware. I encourage you to run it yourself -- your numbers will vary based on Redis/MySQL latency, PHP version, and hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The performance difference between database-backed and Redis-backed permission checking comes down to three architectural decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;O(1) vs O(n) lookups&lt;/strong&gt; -- Redis &lt;code&gt;SISMEMBER&lt;/code&gt; vs array scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surgical vs nuclear invalidation&lt;/strong&gt; -- rewarm one user vs flush everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-layer caching&lt;/strong&gt; -- in-memory + Redis vs cache driver only&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For small applications, these differences are academic. For high-traffic Laravel APIs, they're the difference between adding more servers and optimizing what you have.&lt;/p&gt;

&lt;p&gt;The package is free, MIT-licensed, and available on Packagist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require scabarcas/laravel-permissions-redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; for documentation, migration guides, and the full test suite. Issues and PRs are welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All benchmarks were run on a MacBook Pro M2 with local Redis and MySQL. Production numbers will vary based on network topology and hardware. The &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;benchmark repository&lt;/a&gt; includes instructions for reproducing on your own hardware.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>redis</category>
      <category>performance</category>
      <category>benchmarks</category>
    </item>
    <item>
      <title>Why I Built a Redis-Backed Alternative to Spatie Permissions</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Tue, 31 Mar 2026 16:51:50 +0000</pubDate>
      <link>https://forem.com/scabarcas/why-i-built-a-redis-backed-alternative-to-spatie-permissions-jgo</link>
      <guid>https://forem.com/scabarcas/why-i-built-a-redis-backed-alternative-to-spatie-permissions-jgo</guid>
      <description>&lt;p&gt;If you've worked with Laravel for more than a week, you've probably installed &lt;code&gt;spatie/laravel-permission&lt;/code&gt;. It's the default answer to "how do I add roles and permissions to my app?" -- and for good reason. It's well-maintained, well-documented, and battle-tested.&lt;/p&gt;

&lt;p&gt;So why did I build &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;laravel-permissions-redis&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Because at scale, the database becomes the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept hitting
&lt;/h2&gt;

&lt;p&gt;I was building a multi-tenant SaaS with a Laravel API serving ~200 requests/second. Each request triggered between 5 and 30 permission checks -- middleware, policies, Blade directives, inline gates. With Spatie, every single one of those checks was hitting the cache driver, deserializing a full array of permissions, and scanning it linearly.&lt;/p&gt;

&lt;p&gt;On cold cache? Full database reload. On cache invalidation? Forget everything, rebuild from scratch on the next request. With 50,000+ users, that "next request" was painfully slow.&lt;/p&gt;

&lt;p&gt;The numbers looked something like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;DB queries (Spatie)&lt;/th&gt;
&lt;th&gt;DB queries (Redis approach)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 request, 33 checks&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 requests&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 requests&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the initial cache warm, all permission checks with Redis resolve with &lt;strong&gt;zero additional database queries&lt;/strong&gt;. Not "fewer queries" -- zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use Redis as a Laravel cache driver?
&lt;/h2&gt;

&lt;p&gt;That was my first thought too. Set &lt;code&gt;CACHE_DRIVER=redis&lt;/code&gt; and let Spatie's existing cache layer benefit from Redis speed.&lt;/p&gt;

&lt;p&gt;It helps, but it doesn't solve the architectural problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O(n) lookups.&lt;/strong&gt; Spatie serializes all permissions into a single cached array. Each &lt;code&gt;hasPermissionTo()&lt;/code&gt; call deserializes that array and scans it. With 200 permissions per user, that's 200 comparisons per check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Nuclear invalidation.&lt;/strong&gt; When any permission changes, Spatie calls &lt;code&gt;forgetCachedPermissions()&lt;/code&gt; which drops the &lt;em&gt;entire&lt;/em&gt; cache. Every user pays the cold-start penalty on their next request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No in-memory request cache.&lt;/strong&gt; Even with Redis as the cache driver, Spatie hits Redis on every single check within the same request. If your middleware checks 5 permissions, that's 5 Redis round-trips.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The architecture of laravel-permissions-redis
&lt;/h2&gt;

&lt;p&gt;I took a fundamentally different approach using Redis SET data structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth:user:{userId}:permissions  -&amp;gt;  SET {"posts.create", "posts.edit", "users.view"}
auth:user:{userId}:roles        -&amp;gt;  SET {"admin", "editor"}
auth:role:{roleId}:permissions  -&amp;gt;  SET {"posts.create", "posts.edit"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each permission check is a single &lt;code&gt;SISMEMBER&lt;/code&gt; command -- O(1) time complexity, regardless of how many permissions exist. No deserialization, no scanning.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dual-layer cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request hits middleware
    -&amp;gt; Check in-memory PHP array (zero I/O)
    -&amp;gt; Miss? Check Redis SET (one SISMEMBER)
    -&amp;gt; Miss? Query database, warm Redis, populate in-memory cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a single request, the first check might hit Redis. Every subsequent check for the same user resolves from a PHP array in memory -- no I/O at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surgical invalidation
&lt;/h3&gt;

&lt;p&gt;When a user's permissions change, we don't flush everything. We rewarm only the affected user's Redis SETs. Everyone else's cache stays warm. No cold-start stampede.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spatie: nuke everything&lt;/span&gt;
&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spatie.permission.cache'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// laravel-permissions-redis: rewarm only what changed&lt;/span&gt;
&lt;span class="nv"&gt;$repository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warmUserCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Feature parity (and beyond)
&lt;/h2&gt;

&lt;p&gt;I didn't want this to be a "fast but limited" alternative. The goal was full feature parity with Spatie plus the features I kept needing:&lt;/p&gt;

&lt;h3&gt;
  
  
  Drop-in API compatibility
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These work exactly the same as Spatie&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;givePermissionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.create'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasPermissionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.edit'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasAnyRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same method names, same Blade directives (&lt;code&gt;@role&lt;/code&gt;, &lt;code&gt;@hasanyrole&lt;/code&gt;), same middleware (&lt;code&gt;permission:posts.create&lt;/code&gt;). Migrating is mostly swapping a trait and a config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features Spatie doesn't have
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Octane support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic in-memory cache reset between requests in long-lived workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-tenancy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis key isolation per tenant, with built-in Stancl/Tenancy resolver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wildcard permissions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;posts.*&lt;/code&gt; matches &lt;code&gt;posts.create&lt;/code&gt;, &lt;code&gt;posts.edit&lt;/code&gt;, etc. via &lt;code&gt;fnmatch()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Super admin role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One config value to make a role bypass all permission checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing trait&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;actingAsWithPermissions($user, ['posts.create'])&lt;/code&gt; -- one-liner test setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache warming CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php artisan permissions-redis:warm&lt;/code&gt; to pre-warm all users on deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Seed command&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php artisan permissions-redis:seed&lt;/code&gt; reads from config, supports &lt;code&gt;--fresh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Migrate from Spatie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php artisan permissions-redis:migrate-from-spatie&lt;/code&gt; with &lt;code&gt;--dry-run&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fluent guard scoping&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$user-&amp;gt;forGuard('api')-&amp;gt;hasPermissionTo('posts.create')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Automated migration from Spatie
&lt;/h3&gt;

&lt;p&gt;This was important to me. If you already have Spatie installed with production data, migration needs to be painless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See what would happen without changing anything&lt;/span&gt;
php artisan permissions-redis:migrate-from-spatie &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Run the migration&lt;/span&gt;
php artisan permissions-redis:migrate-from-spatie
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command reads Spatie's tables, creates equivalent records in the new schema, and warms the Redis cache. There's a &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis/blob/main/docs/migration-from-spatie.md" rel="noopener noreferrer"&gt;full migration guide&lt;/a&gt; with a method equivalence table and behavior differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenancy without the headache
&lt;/h2&gt;

&lt;p&gt;Spatie's "teams" feature works but adds a &lt;code&gt;team_id&lt;/code&gt; column to every pivot table and requires careful query scoping.&lt;/p&gt;

&lt;p&gt;With Redis, tenant isolation is simpler -- prefix the keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth:user:t:{tenantId}:{userId}:permissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/permissions-redis.php&lt;/span&gt;
&lt;span class="s1"&gt;'tenancy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'enabled'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'resolver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stancl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or any callable&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complete data isolation. No query scoping to forget. No cross-tenant leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Octane: the problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;Laravel Octane keeps your application in memory across requests. Great for performance, terrible for anything that caches state in PHP properties.&lt;/p&gt;

&lt;p&gt;Spatie caches permissions in static properties. In Octane, User A's permissions can leak into User B's request if they hit the same worker. The Spatie team &lt;a href="https://spatie.be/docs/laravel-permission/v6/advanced-usage/cache#content-cache-identifier" rel="noopener noreferrer"&gt;acknowledges this&lt;/a&gt; and suggests workarounds, but there's no built-in solution.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;laravel-permissions-redis&lt;/code&gt; listens for Octane's &lt;code&gt;RequestReceived&lt;/code&gt; event and resets all in-memory state automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/permissions-redis.php&lt;/span&gt;
&lt;span class="s1"&gt;'octane'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'reset_on_request'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No leaked state. No workarounds. It just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest trade-offs
&lt;/h2&gt;

&lt;p&gt;This package is not for everyone. Here's when you should &lt;strong&gt;not&lt;/strong&gt; use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You don't run Redis.&lt;/strong&gt; This package requires Redis. If your stack is MySQL + file cache, Spatie is the right choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need &lt;code&gt;getDirectPermissions()&lt;/code&gt; vs &lt;code&gt;getPermissionsViaRoles()&lt;/code&gt;.&lt;/strong&gt; This package merges all permissions into a flat set. If you need to distinguish "was this permission assigned directly or via a role?", Spatie handles that better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to support PHP 8.0 or Laravel 8.&lt;/strong&gt; This package requires PHP 8.3+ and Laravel 11+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization isn't your bottleneck.&lt;/strong&gt; If you're doing 10 requests/second with 3 permission checks each, Spatie is fast enough. Don't add Redis complexity for no measurable gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require scabarcas/laravel-permissions-redis

php artisan vendor:publish &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Scabarcas&lt;/span&gt;&lt;span class="se"&gt;\L&lt;/span&gt;&lt;span class="s2"&gt;aravelPermissionsRedis&lt;/span&gt;&lt;span class="se"&gt;\L&lt;/span&gt;&lt;span class="s2"&gt;aravelPermissionsRedisServiceProvider"&lt;/span&gt;

php artisan migrate

&lt;span class="c"&gt;# Optional: warm cache for all existing users&lt;/span&gt;
php artisan permissions-redis:warm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the trait to your User model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Scabarcas\LaravelPermissionsRedis\Traits\HasRedisPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasRedisPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The API is intentionally familiar. If you've used Spatie, you already know how to use this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The package is at &lt;strong&gt;v3.0.0&lt;/strong&gt; with full support for PHP 8.3/8.4, Laravel 11/12/13, and Redis 6/7. The test suite covers UUID/ULID models, Octane workers, multi-tenant isolation, and integration tests against real Redis instances in CI.&lt;/p&gt;

&lt;p&gt;I'd love feedback, issues, and PRs. The repo is at &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;github.com/scabarcas17/laravel-permissions-redis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're hitting performance walls with database-backed permissions, give it a try. Your Redis server is already sitting there -- might as well put it to work.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>redis</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Solved Multi-Guard Permission Issues in Laravel with Redis</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 16:11:07 +0000</pubDate>
      <link>https://forem.com/scabarcas/how-i-solved-multi-guard-permission-issues-in-laravel-with-redis-20d9</link>
      <guid>https://forem.com/scabarcas/how-i-solved-multi-guard-permission-issues-in-laravel-with-redis-20d9</guid>
      <description>&lt;p&gt;When working with Laravel applications that use multiple guards (web, api, etc.), I ran into a subtle but critical issue:&lt;/p&gt;

&lt;p&gt;Permissions were leaking between guards.&lt;/p&gt;

&lt;p&gt;At first, everything seemed fine… until it wasn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine this scenario:&lt;/p&gt;

&lt;p&gt;A user has a permission under the api guard&lt;br&gt;
You check that permission under the web guard&lt;br&gt;
$user-&amp;gt;hasPermissionTo('posts.edit');&lt;/p&gt;

&lt;p&gt;And it returns true, even though it shouldn’t.&lt;/p&gt;

&lt;p&gt;This happens when your permission system doesn’t properly isolate guards — something that can silently introduce security issues in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Happens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most implementations treat permission names as globally unique:&lt;/p&gt;

&lt;p&gt;posts.edit&lt;/p&gt;

&lt;p&gt;But in reality, permissions should be scoped by guard, meaning:&lt;/p&gt;

&lt;p&gt;web: posts.edit&lt;br&gt;
api: posts.edit&lt;/p&gt;

&lt;p&gt;Without that separation, collisions are inevitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In v2.0.0 of my package (laravel-permissions-redis), I redesigned the permission system to be fully guard-aware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Guard-Scoped Permission Checks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All permission and role checks now accept a guard:&lt;/p&gt;

&lt;p&gt;$user-&amp;gt;hasPermissionTo('posts.edit', 'api');&lt;br&gt;
$user-&amp;gt;hasRole('admin', 'web');&lt;/p&gt;

&lt;p&gt;Or fluently:&lt;/p&gt;

&lt;p&gt;$user-&amp;gt;forGuard('api')-&amp;gt;hasPermissionTo('posts.edit');&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Redis Storage Redesign&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Permissions are now stored using this format:&lt;/p&gt;

&lt;p&gt;guard|permission&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;api|posts.edit&lt;br&gt;
web|posts.edit&lt;/p&gt;

&lt;p&gt;This completely eliminates collisions between guards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Smarter Caching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also improved cache control to make the system more scalable:&lt;/p&gt;

&lt;p&gt;rewarmAll() → rebuild cache without flushing&lt;br&gt;
warmPermissionAffectedUsers() → only warm impacted users&lt;br&gt;
getUserIdsAffectedByPermission() → precise impact analysis&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Performance Improvements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No more full table scans when warming users&lt;br&gt;
Redis config is cached internally&lt;br&gt;
Middleware now resolves the correct guard automatically&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking Changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're upgrading:&lt;/p&gt;

&lt;p&gt;Methods like hasPermission() now accept a $guard&lt;br&gt;
Redis key format changed → you must flush and rewarm cache&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Authorization bugs are dangerous because they don’t crash your app — they silently give access.&lt;/p&gt;

&lt;p&gt;Fixing guard isolation is not just a “nice to have”, it’s essential for any system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIs + Web apps&lt;/li&gt;
&lt;li&gt;Multi-auth setups&lt;/li&gt;
&lt;li&gt;Scalable architectures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This update was focused on one thing:&lt;/p&gt;

&lt;p&gt;Making authorization correct, predictable, and scalable.&lt;/p&gt;

&lt;p&gt;If you're dealing with complex permission systems in Laravel, this approach might save you from some very tricky bugs.&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts or how you're handling permissions in your projects.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>redis</category>
      <category>php</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Reducing Laravel Permission Queries Using Redis (Benchmark Results)</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Tue, 24 Mar 2026 16:32:51 +0000</pubDate>
      <link>https://forem.com/scabarcas/reducing-laravel-permission-queries-using-redis-benchmark-results-4dmj</link>
      <guid>https://forem.com/scabarcas/reducing-laravel-permission-queries-using-redis-benchmark-results-4dmj</guid>
      <description>&lt;p&gt;Laravel permissions work great… until your application starts to scale.&lt;/p&gt;

&lt;p&gt;If you're using role/permission checks heavily, you might be hitting your database more often than you think.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you a simple benchmark comparing the default behavior vs a Redis-based approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In many Laravel applications, permission checks look like this:&lt;/p&gt;

&lt;p&gt;$user-&amp;gt;can('edit-post');&lt;/p&gt;

&lt;p&gt;Looks harmless, right?&lt;br&gt;
But under the hood, this can trigger multiple database queries, especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have many users&lt;/li&gt;
&lt;li&gt;Complex role/permission structures&lt;/li&gt;
&lt;li&gt;Frequent authorization checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At small scale, it’s fine.&lt;br&gt;
At large scale… it adds up quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To test this, I created a simple benchmark comparing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default Laravel permissions behavior&lt;/li&gt;
&lt;li&gt;Redis-cached permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmark repo: &lt;a href="https://github.com/scabarcas17/laravel-permissions-redis-benchmark" rel="noopener noreferrer"&gt;https://github.com/scabarcas17/laravel-permissions-redis-benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run multiple permission checks&lt;/li&gt;
&lt;li&gt;Measure database queries&lt;/li&gt;
&lt;li&gt;Compare performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Default Behavior&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple database queries per permission check&lt;/li&gt;
&lt;li&gt;Repeated queries for the same permissions&lt;/li&gt;
&lt;li&gt;Increased load under high traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;With Redis&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Permissions cached in Redis&lt;/li&gt;
&lt;li&gt;Near-zero database queries after first load&lt;/li&gt;
&lt;li&gt;Much faster response times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The biggest issue is not the first query…&lt;br&gt;
It’s the repeated queries for the same permissions.&lt;br&gt;
By caching permissions in Redis, we eliminate redundant database access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To test this approach in a real scenario, I built a small package: &lt;a href="https://packagist.org/packages/scabarcas/laravel-permissions-redis" rel="noopener noreferrer"&gt;https://packagist.org/packages/scabarcas/laravel-permissions-redis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub repo:&lt;br&gt;
&lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;https://github.com/scabarcas17/laravel-permissions-redis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This package adds a Redis layer on top of Laravel permissions, reducing unnecessary queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When Does This Matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This approach is especially useful if your app has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High traffic&lt;/li&gt;
&lt;li&gt;Many permission checks per request&lt;/li&gt;
&lt;li&gt;Complex role/permission structures&lt;/li&gt;
&lt;li&gt;Performance bottlenecks related to authorization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel’s default behavior is solid and works well for most applications.&lt;/p&gt;

&lt;p&gt;But if you're scaling and noticing performance issues, caching permissions can make a real difference.&lt;/p&gt;

&lt;p&gt;This benchmark is just a starting point—but it clearly shows the impact of reducing repeated database queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have you experienced performance issues with permissions?&lt;/li&gt;
&lt;li&gt;How are you handling caching in your apps?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>performance</category>
      <category>redis</category>
    </item>
    <item>
      <title>How I Eliminated Repetitive Permission Queries in Laravel Using Redis</title>
      <dc:creator>Sebastian Cabarcas</dc:creator>
      <pubDate>Thu, 19 Mar 2026 19:27:35 +0000</pubDate>
      <link>https://forem.com/scabarcas/how-i-eliminated-repetitive-permission-queries-in-laravel-using-redis-4037</link>
      <guid>https://forem.com/scabarcas/how-i-eliminated-repetitive-permission-queries-in-laravel-using-redis-4037</guid>
      <description>&lt;p&gt;One of the most common performance issues I’ve seen in Laravel applications is related to roles and permissions.&lt;/p&gt;

&lt;p&gt;At first, everything works fine.&lt;/p&gt;

&lt;p&gt;But as the application grows, authorization checks become more frequent — and suddenly, your database is handling a large number of repetitive queries just to validate permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
Even when using caching strategies, many applications still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hit the database frequently for permission checks&lt;/li&gt;
&lt;li&gt;Recompute authorization logic on every request&lt;/li&gt;
&lt;li&gt;Struggle under high load scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This becomes especially noticeable in systems with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex role structures&lt;/li&gt;
&lt;li&gt;Multiple middleware checks&lt;/li&gt;
&lt;li&gt;High traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of relying on the database (even with caching), I explored a different approach:&lt;/p&gt;

&lt;p&gt;Move roles and permissions entirely into Redis&lt;/p&gt;

&lt;p&gt;The goal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep authorization in-memory&lt;/li&gt;
&lt;li&gt;Eliminate repetitive queries&lt;/li&gt;
&lt;li&gt;Improve response times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store roles and permissions in Redis&lt;/li&gt;
&lt;li&gt;Resolve all authorization checks from memory&lt;/li&gt;
&lt;li&gt;Avoid hitting the database during request lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows permission checks to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster&lt;/li&gt;
&lt;li&gt;More scalable&lt;/li&gt;
&lt;li&gt;More predictable under load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a package around this idea:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/scabarcas17/laravel-permissions-redis" rel="noopener noreferrer"&gt;https://github.com/scabarcas17/laravel-permissions-redis&lt;/a&gt;&lt;br&gt;
&lt;a href="https://packagist.org/packages/scabarcas/laravel-permissions-redis" rel="noopener noreferrer"&gt;https://packagist.org/packages/scabarcas/laravel-permissions-redis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s designed to integrate naturally with Laravel while replacing repetitive database queries with Redis-based resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When Does This Make Sense?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This approach is especially useful if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app performs frequent permission checks&lt;/li&gt;
&lt;li&gt;You are scaling and need better performance&lt;/li&gt;
&lt;li&gt;You want to reduce database load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel is not the problem — architecture is.&lt;/p&gt;

&lt;p&gt;Small decisions like how you handle permissions can have a huge impact as your system grows.&lt;/p&gt;

&lt;p&gt;I’d love to hear how others are solving this problem or any feedback on this approach.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>laravel</category>
      <category>performance</category>
      <category>php</category>
    </item>
  </channel>
</rss>
