
Released v4.0 stable of a Redis-backed roles & permissions package for Laravel — drop-in API compatible with Spatie
TL;DR: Just released v4.0.0 stable of laravel-permissions-redis, a Redis-backed alternative to spatie/laravel-permission. Same API (hasRole, hasPermissionTo, @permission, middleware), different cache strategy: every user→roles→permissions mapping lives in Redis as a SET, and checks become O(1) SISMEMBER calls. ~10x faster on the median request vs Spatie on identical workloads. Three weeks in production without surfacing a new bug.
Why I built it
Spatie is excellent and remains the default — but in apps with heavy authorization on the hot path I kept hitting the same pattern: every request lazy-loads the authenticated user's roles and permissions (one DB query) and then deserializes the cached permission array and scans it. Fine for most apps. Painful for high-traffic APIs.
I wanted a package where the authorization check itself is a constant-time Redis lookup and where invalidation is surgical, not "drop the entire cache and let the next N requests pay the rebuild cost simultaneously".
How it differs
| Aspect | spatie/laravel-permission | laravel-permissions-redis |
|---|---|---|
| Warm check | Deserialize cached array → scan for match | SISMEMBER (O(1)) |
| Cache scope | Global permission registry | Per-user/per-role SETs |
| Invalidation | forgetCachedPermissions() — drops everything |
Rewarm only affected user/role |
| After invalidation | Next request rebuilds full cache | Cache is already warm |
| Per-request | Hits cache driver every call | In-memory cache, no repeated Redis |
Benchmarks
Numbers from a standalone benchmark app that runs both packages side-by-side. Apple Silicon, PHP 8.4, predis, 5 warm-ups + 30 measurement runs, GC reset between runs, median reported.
Scenario (1 iter = 27 hasPermissionTo + 4 hasRole + 4 batch ops + 2 collections) |
Spatie | This package | Speedup |
|---|---|---|---|
| 1 iteration | 14.27 ms | 1.44 ms | 9.92x |
| 10 iterations | 144.38 ms | 14.39 ms | 10.03x |
| 50 iterations | 730.88 ms | 72.87 ms | 10.03x |
DB queries per request: 4 → 1 (75% reduction).
The ~10x holds because both strategies scale linearly — what differs is the per-iteration constant (4 DB queries vs 1 Redis lookup).
What 4.0 brings
- Permission group metadata preserved in Redis with atomic rebuild (
replacePermissionGroups) - Guard-aware Blade directives and full multi-guard isolation
- Queue-backed cache warming (
WarmUserCacheJob) permissions-redis:migrate-from-spatieartisan command for one-shot data migration- Contract suite running against a real Redis instance (not mocks) — auto-skips when Redis isn't available
- Multi-tenancy (
stancl/tenancyintegration + custom resolvers), UUID/ULID support, Laravel Octane support
PHP 8.3+, Laravel 11/12/13, PHPStan level max, Pest 4, mutation testing via Infection.
When NOT to use it
Honest list, because this matters more than the speedups:
- You don't run Redis. This package requires it. If Redis isn't in your stack, Spatie's database/file-cache approach is the right choice.
- You need
getDirectPermissions()/getPermissionsViaRoles()at runtime. This package merges them by design. - You need Spatie's
teamsfeature. The multi-tenancy here is different — Redis namespace isolation, not team-scoped role assignments. - You're on PHP < 8.3 or Laravel < 11. No backport planned.
- Authorization isn't a bottleneck. If your app is low-traffic, the DB-backed approach is fine — don't add Redis just for this.
Links
- Repo: https://github.com/scabarcas17/laravel-permissions-redis
- Packagist: https://packagist.org/packages/scabarcas/laravel-permissions-redis
- Benchmark app: https://github.com/scabarcas17/laravel-permissions-redis-benchmark
- Migration guide from Spatie: in the repo's
docs/folder
Happy to answer architecture questions or hear what breaks if you try it. Roast welcome.