In Laravel 13, which cache stores can back Cache::lock() across servers, and where do the database and Redis stores keep their locks?
answer
- every server must share one store
- file: one server; array: one process
- cache_locks: key, owner, expiration
- lock_connection, lock_table, lock_timeout 86400
- Redis locks on the default connection
basics
~20 sCross-server locks need a shared store: database, redis, memcached or dynamodb; file covers one machine and array one process. The Laravel 13 database default uses the cache_locks table; Redis uses its lock_connection, the default connection.
solid answer
~40 s`Cache::lock()` is forwarded to the current store, so the lock is only as shared as the store. `database`, `redis`, `memcached` and `dynamodb` are central and coordinate every server; `file` writes lock files to local disk and `array` keeps them in process memory. The Laravel 13 skeleton's `database` store writes locks to `cache_locks` (`key`, `owner`, `expiration`), created by the default migration; `lock_connection` and `lock_table` can move them, a 0-second lock gets `lock_timeout` (86400 s), and a 2-in-100 lottery prunes expired rows. The `redis` store keeps cached values on the `cache` connection but locks on `lock_connection`, which is `default`. That is why `cache:clear --locks` on Redis runs `FLUSHDB` on the default connection, where Redis queues also live by default.
code
ini · 8 lines# Database store: locks on a separate connection and table
CACHE_STORE=database
DB_CACHE_LOCK_CONNECTION=locks_db
DB_CACHE_LOCK_TABLE=statement_locks
# Redis store: give locks their own connection instead of 'default'
# CACHE_STORE=redis
# REDIS_CACHE_LOCK_CONNECTION=locksgo deeper
Recall that locks need a shared store such as database or Redis, and that the Laravel 13 default keeps them in the cache_locks table.
Explain why file and array locks are local, how the database store inserts, updates and prunes cache_locks rows, and what lock_connection changes.
Isolate lock storage from queues and cache data, recognise that cache:clear --locks can FLUSHDB the default Redis connection, and make every worker use one store.
Choose whether locks belong in the primary database, a Redis instance or a dedicated connection, balancing blast radius, latency and operational ownership.
## A lock is only as shared as its store `Cache::lock()` is not a separate service. The `Cache` repository forwards `lock()` to the underlying store, and the store decides where the lock lives. For two queue workers on two servers to exclude each other while generating the same statement, both must read and write the **same** lock record. Laravel's documentation says so directly: locks need one of the `memcached`, `redis`, `dynamodb`, `database`, `file` or `array` drivers, and all servers must talk to the same central cache server. | Store | Where locks live | Scope of exclusion | |---|---|---| | `database` | `cache_locks` table (configurable) | every server using that database | | `redis` | keys on the lock connection | every server using that Redis | | `memcached` | keys on the Memcached servers | every server using that pool | | `dynamodb` | items in the cache table | every server using that table | | `file` | lock files under `lock_path` on local disk | one machine | | `array` | PHP memory | one process | The `file` and `array` rows are the trap. They **work** — `get()` returns `true` — so a single-server development setup looks fine, but two servers, or two workers with the `array` store, never see each other's locks. ## The database store and `cache_locks` The Laravel 13 skeleton defaults to `CACHE_STORE=database`, and its migration `0001_01_01_000001_create_cache_table` creates two tables: `cache` and `cache_locks` (`key` primary, `owner`, `expiration` indexed). Mechanics: 1. **Acquire** inserts a row. A duplicate key means someone holds it, so Laravel tries an `UPDATE` that succeeds only if the row has **expired** or already belongs to **this owner**. 2. **Release** deletes the row `WHERE key = ? AND owner = ?`. 3. **A 0-second lock** is stored with an expiration of `lock_timeout` seconds, 86400 by default, so it cannot live forever. 4. **Pruning**: on each acquire, a lottery (`lock_lottery`, default 2 in 100) deletes expired rows. Configuration keys on the `database` store entry: - `lock_connection` (`DB_CACHE_LOCK_CONNECTION`) — put locks on another database connection; - `lock_table` (`DB_CACHE_LOCK_TABLE`, falling back to `cache_locks`); - `lock_timeout` and `lock_lottery` — read by the store if you add them. Lock names get the cache prefix, so two apps sharing the table do not collide on the same name. ## The Redis store and `lock_connection` The skeleton's `redis` store uses `connection => 'cache'` (Redis database 1 by default) for values and `lock_connection => 'default'` (database 0) for locks. A lock with a positive lifetime is a set-if-not-exists with an expiry; a 0-second lock is set **without** an expiry. This split matters for clean-up: - `php artisan cache:clear` flushes the cache connection and leaves locks alone. - `php artisan cache:clear --locks` calls `flushLocks()`, which on Redis runs **`FLUSHDB` on the lock connection**. With the defaults that is the `default` connection — the same one the skeleton's `redis` queue connection uses. On an app with Redis queues, that command can wipe pending jobs along with the locks. - Both stores refuse `flushLocks()` with a `RuntimeException` when locks share the cache's own table or connection. A dedicated Redis connection for locks, set through `REDIS_CACHE_LOCK_CONNECTION`, removes that coupling. ## Practical checklist - Every server and worker must resolve the same store for the same lock name: set `CACHE_STORE` consistently, or name the store explicitly with `Cache::store('redis')->lock(...)`. - Keep the `cache_locks` migration if you use the database store; without it acquiring a lock fails with a missing-table error. - Prefer a shared store over `file` as soon as there is more than one server. - Treat `cache:clear --locks` on Redis as a production-dangerous command unless locks have their own connection. ## What tests can and cannot show The skeleton's `phpunit.xml` switches the cache to the `array` store. Array locks behave correctly **inside one process**: a second `get()` on the same name returns `false`, `release()` checks the owner and `refresh()` extends the in-memory expiry. That is enough to test your own acquire-and-release logic. It says nothing about cross-server behaviour, a missing `cache_locks` table or a mis-set `lock_connection`, which only an environment that uses the real store will reveal.
- In Laravel, why do statement jobs on two servers both acquire the same lock when CACHE_STORE=file?The file store writes locks as files under its `lock_path` on each server's local disk. Each server sees only its own files, so both acquisitions succeed. Locks that must span machines need a central store such as the database, Redis, Memcached or DynamoDB.
- In Laravel, why can php artisan cache:clear --locks on the Redis store delete queued jobs?`flushLocks()` on the Redis store runs `FLUSHDB` on the lock connection. The skeleton sets the Redis store's `lock_connection` to `default`, which is also the connection the `redis` queue uses by default, so the whole database, jobs included, is flushed. Give locks a dedicated connection before relying on that command.
saying these in an interview costs you the question
- Any cache store gives cluster-wide locks as long as Cache::lock() returns true
- The database store keeps locks in the same cache table as values
- cache:clear --locks on Redis deletes only lock keys
- A 0-second lock on the database store never expires
- Redis cache values and Redis locks share one connection by default