<?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>DEV Community: Sebastian Cabarcas</title>
    <description>The latest articles on DEV Community by Sebastian Cabarcas (@scabarcas).</description>
    <link>https://dev.to/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>DEV Community: Sebastian Cabarcas</title>
      <link>https://dev.to/scabarcas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scabarcas"/>
    <language>en</language>
    <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://dev.to/scabarcas/benchmarking-laravel-permission-checks-database-vs-redis-3al7</link>
      <guid>https://dev.to/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://dev.to/scabarcas/why-i-built-a-redis-backed-alternative-to-spatie-permissions-jgo</link>
      <guid>https://dev.to/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://dev.to/scabarcas/how-i-solved-multi-guard-permission-issues-in-laravel-with-redis-20d9</link>
      <guid>https://dev.to/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://dev.to/scabarcas/reducing-laravel-permission-queries-using-redis-benchmark-results-4dmj</link>
      <guid>https://dev.to/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://dev.to/scabarcas/how-i-eliminated-repetitive-permission-queries-in-laravel-using-redis-4037</link>
      <guid>https://dev.to/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>
