The package automatically caches all permissions for 24 hours. Configure cache settings for optimal performance.
config/permission.php
return [ 'cache' => [ /* * By default all permissions are cached for 24 hours to speed up performance. * When permissions or roles are updated the cache is flushed automatically. */ 'expiration_time' => \DateInterval::createFromDateString('24 hours'), /* * The cache key used to store all permissions. */ 'key' => 'spatie.permission.cache', /* * You may optionally indicate a specific cache driver to use for permission and * role caching using any of the `store` drivers listed in the cache.php config * file. Using 'default' here means to use the `default` set in cache.php. */ 'store' => 'default', ],];
For high-traffic applications, use Redis for permission caching:
The cache is automatically flushed when permissions or roles are modified. No manual intervention needed.
// Cache is automatically flushed after these operations:Permission::create(['name' => 'new-permission']); // Cache flushed$permission->update(['name' => 'updated-name']); // Cache flushed$role->givePermissionTo('edit-articles'); // Cache flushed$role->removePermission('edit-articles'); // Cache flushed// Cache is NOT flushed for user-level changes (performance optimization):$user->givePermissionTo('edit-articles'); // Cache NOT flushed ✓$user->assignRole('admin'); // Cache NOT flushed ✓
User-level permission assignments don’t flush the cache because they don’t change the global permission structure. This is a performance optimization since user operations are more frequent.
The package optimizes cache size by aliasing column names and excluding unnecessary fields.
config/permission.php
return [ 'cache' => [ /* * Columns to exclude from cache to reduce size. * Timestamps are typically not needed for permission checks. */ 'column_names_except' => ['created_at', 'updated_at', 'deleted_at'], ],];
How Cache Aliasing Works
The PermissionRegistrar uses single-letter aliases for column names to reduce cache size:
Always eager load roles and permissions to avoid N+1 query problems.
// ❌ Bad: N+1 queries$users = User::all();foreach ($users as $user) { if ($user->hasPermissionTo('edit-articles')) { // Queries executed for each user }}// ✅ Good: Eager loading$users = User::with(['roles.permissions', 'permissions'])->get();foreach ($users as $user) { if ($user->hasPermissionTo('edit-articles')) { // Uses preloaded data }}
The package caches permission data globally, but eager loading user relationships still provides significant performance benefits:
// Load user with relationships before checking$user->loadMissing('roles', 'permissions');// Now permission checks use cached data + preloaded relationships$user->hasPermissionTo('edit-articles'); // Optimized
The package implements thread-safe permission loading to prevent cache stampede in concurrent environments.
// From PermissionRegistrar.php:175private function loadPermissions(int $retries = 0): void{ // First check - fast path for already loaded if ($this->permissions) { return; } // Prevent concurrent loading using flag-based lock if ($this->isLoadingPermissions && $retries < 10) { usleep(10000); // Wait 10ms $retries++; $this->loadPermissions($retries); return; } $this->isLoadingPermissions = true; // Load permissions... $this->isLoadingPermissions = false;}
This implementation prevents duplicate database queries when multiple requests hit the cache simultaneously. Essential for Laravel Octane, Swoole, and high-concurrency applications.
Configure automatic cache resets for Octane/Swoole long-running processes.
config/permission.php
return [ /* * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. */ 'register_octane_reset_listener' => false,];
For Octane applications, the package automatically clears in-memory permission collections:
// Automatically called on each request in Octanepublic function clearPermissionsCollection(): void{ $this->permissions = null; $this->wildcardPermissionsIndex = []; $this->isLoadingPermissions = false;}
Only enable register_octane_reset_listener if you experience stale permission data.
Check multiple permissions in a single call to reduce overhead.
// ✅ Good: Single check for multiple permissionsif ($user->hasAllPermissions(['edit-articles', 'publish-articles', 'delete-articles'])) { // User has all permissions}if ($user->hasAnyPermission(['edit-articles', 'view-articles'])) { // User has at least one permission}// ❌ Bad: Multiple individual checksif ($user->hasPermissionTo('edit-articles') && $user->hasPermissionTo('publish-articles') && $user->hasPermissionTo('delete-articles')) { // More overhead}
Manually flush cache when needed for bulk operations.
use Spatie\Permission\PermissionRegistrar;// After bulk permission changesDB::transaction(function () { // Bulk operations... Permission::insert([...]); Role::insert([...]);});// Manually flush cacheapp(PermissionRegistrar::class)->forgetCachedPermissions();// Or via ArtisanArtisan::call('permission:cache-reset');
The cache is shared across all application instances. When using multiple servers, ensure your cache driver (Redis, Memcached) is shared, or cache invalidation won’t propagate.
-- model_has_permissions tableCREATE INDEX model_has_permissions_model_id_model_type_index ON model_has_permissions(model_id, model_type);CREATE INDEX model_has_permissions_permission_id_index ON model_has_permissions(permission_id);CREATE INDEX model_has_permissions_team_id_index ON model_has_permissions(team_id); -- if using teams-- model_has_roles tableCREATE INDEX model_has_roles_model_id_model_type_index ON model_has_roles(model_id, model_type);CREATE INDEX model_has_roles_role_id_index ON model_has_roles(role_id);CREATE INDEX model_has_roles_team_id_index ON model_has_roles(team_id); -- if using teams-- role_has_permissions tableCREATE INDEX role_has_permissions_role_id_index ON role_has_permissions(role_id);CREATE INDEX role_has_permissions_permission_id_index ON role_has_permissions(permission_id);
These indexes are automatically created by the package migrations.
Store permissions on roles rather than directly on users when possible.
// ✅ Good: Permission through role (fewer database rows)$role = Role::create(['name' => 'editor']);$role->givePermissionTo(['edit-articles', 'publish-articles']);$user->assignRole('editor'); // 1 row in model_has_roles// ⚠️ Less efficient: Direct permissions (more database rows)$user->givePermissionTo(['edit-articles', 'publish-articles']); // 2 rows in model_has_permissions
With role-based permissions, you have fewer rows in pivot tables and simpler permission management. However, direct permissions are still valuable for user-specific overrides.