Redlock vs PostgreSQL Advisory Locks: When You Don't Need Redis for Distributed Locking
We debated Redlock vs Postgres locks after a nasty outage. “We need Redis for distributed locks.” I asked why. “Because that’s how you do distributed locking.” The team was about to add Redis to their stack—five instances for Redlock—purely to prevent duplicate payment processing. They already had PostgreSQL. They didn’t know PostgreSQL had advisory locks.
This is a pattern I’ve seen repeatedly: teams reaching for specialized infrastructure when their existing database already provides the capability they need. PostgreSQL advisory locks are less glamorous than Redlock. They don’t have the same industry buzz. But for most use cases—and especially for teams that already operate PostgreSQL—they’re simpler to deploy, simpler to operate, and actually safer.
The irony is that Redlock, despite its complexity, has known safety issues. Martin Kleppmann’s famous critique “How to do distributed locking” exposed fundamental problems with the algorithm, particularly around clock skew and GC pauses. PostgreSQL advisory locks, by contrast, are simpler and don’t depend on clock synchronization. They have their own limitations—single point of failure if your database is down—but those limitations are usually already addressed by your database HA strategy.
The real question isn’t “Redis vs PostgreSQL.” It’s “do you need to add infrastructure for this, or can you use what you have?” If you already operate PostgreSQL well, advisory locks are essentially free. If you’d have to add Redis just for locking, the operational burden is significant.
Tested on: PostgreSQL 16, Redis 7, Java 21, Go 1.22
When Do You Need Distributed Locks?
Use Cases
1. Preventing duplicate processing
- Payment processing (charge once)
- Job scheduling (run once)
- Data migration (process each row once)
2. Coordinating access to external resources
- Rate-limited APIs (one caller at a time)
- Shared files (exclusive write)
- Hardware devices (single access)
3. Leader election
- Single active instance
- Scheduled tasks (one executor)
When NOT to Use Distributed Locks
❌ Database row-level locking
→ Use SELECT FOR UPDATE or transactions
❌ Preventing race conditions in DB
→ Use database constraints, transactions
❌ Cache stampede prevention
→ Use probabilistic early expiration
❌ Rate limiting
→ Use token bucket or sliding window
PostgreSQL Advisory Locks
Session-Level Locks
-- Acquire lock (blocks if held by another session)
SELECT pg_advisory_lock(12345);
-- Try to acquire (returns immediately)
SELECT pg_try_advisory_lock(12345); -- Returns TRUE or FALSE
-- Release lock
SELECT pg_advisory_unlock(12345);
-- Lock is automatically released when session ends
Transaction-Level Locks
-- Automatically released at end of transaction
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- ... do work ...
COMMIT; -- Lock released automatically
Lock Key Strategies
-- Single integer key
SELECT pg_advisory_lock(hashtext('payment:order_123'));
-- Two integer keys (for namespacing)
SELECT pg_advisory_lock(1, 123); -- (namespace, id)
-- Entity-based locking
SELECT pg_advisory_lock(
hashtext('orders'), -- table
hashtext('order_123') -- id
);
Redis Redlock
Basic Redis Lock
# Single Redis instance (NOT safe for distributed systems)
SET lock:payment:123 owner_id NX EX 30
# NX = Only if not exists
# EX 30 = Expire after 30 seconds
Redlock Algorithm (5 Redis instances)
1. Get current time in milliseconds
2. Try to acquire lock on all N (5) Redis instances
3. Lock acquired if:
- Majority (N/2+1 = 3) instances locked
- Total time < lock validity time
4. Effective lock time = validity - acquisition time
5. If failed, unlock all instances
Comparison
Operational Complexity
| Aspect | PostgreSQL Advisory | Redis Redlock |
|---|---|---|
| Infrastructure | Already have DB | Need 5 Redis instances |
| Failure modes | Single point (DB) | Complex (majority) |
| Clock dependency | No | Yes (problematic!) |
| Network partitions | Simple | Complex |
| Monitoring | Standard DB tools | Additional tooling |
Performance (1000 lock/unlock cycles)
| Metric | PostgreSQL | Redis (single) | Redlock (5) |
|---|---|---|---|
| Acquire p50 | 0.8ms | 0.3ms | 2.5ms |
| Acquire p99 | 3.2ms | 1.1ms | 8.4ms |
| Release p50 | 0.5ms | 0.2ms | 1.8ms |
| Ops/sec | 850 | 2,400 | 280 |
Safety Guarantees
| Property | PostgreSQL | Redis (single) | Redlock |
|---|---|---|---|
| Mutual exclusion | ✅ | ❌* | ⚠️** |
| Deadlock-free | ✅ (timeout) | ✅ (TTL) | ✅ (TTL) |
| Fault-tolerant | ❌ | ❌ | ⚠️*** |
* Redis single: Lock can be held by two processes after failover
** Redlock: Martin Kleppmann's critique shows edge cases
*** Requires majority available, clock sync issues
Implementation
Java - PostgreSQL Advisory Lock
// AdvisoryLock.java
@Repository
public class AdvisoryLockRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public boolean tryLock(String key, Duration timeout) {
long lockId = hashKey(key);
// Try to acquire with timeout
String sql = "SELECT pg_try_advisory_lock(?)";
Boolean acquired = jdbcTemplate.queryForObject(sql, Boolean.class, lockId);
if (acquired != null && acquired) {
return true;
}
// Retry with timeout
long deadline = System.currentTimeMillis() + timeout.toMillis();
while (System.currentTimeMillis() < deadline) {
acquired = jdbcTemplate.queryForObject(sql, Boolean.class, lockId);
if (acquired != null && acquired) {
return true;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public void unlock(String key) {
long lockId = hashKey(key);
jdbcTemplate.execute("SELECT pg_advisory_unlock(" + lockId + ")");
}
private long hashKey(String key) {
return key.hashCode() & 0x7FFFFFFFL; // Positive integer
}
}
// Usage
@Service
public class PaymentService {
@Autowired
private AdvisoryLockRepository lockRepo;
public void processPayment(String orderId) {
String lockKey = "payment:" + orderId;
if (!lockRepo.tryLock(lockKey, Duration.ofSeconds(30))) {
throw new LockAcquisitionException("Could not acquire lock");
}
try {
// Process payment
doPayment(orderId);
} finally {
lockRepo.unlock(lockKey);
}
}
}
Java - Redlock with Redisson
// RedlockService.java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
@Service
public class RedlockService {
@Autowired
private RedissonClient redisson;
public void processPayment(String orderId) {
RLock lock = redisson.getLock("payment:" + orderId);
try {
// Try to acquire lock for 30 seconds, hold for 10 seconds
boolean acquired = lock.tryLock(30, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new LockAcquisitionException("Could not acquire lock");
}
try {
doPayment(orderId);
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
Go - PostgreSQL Advisory Lock
// advisory_lock.go
package lock
import (
"context"
"database/sql"
"hash/fnv"
"time"
)
type AdvisoryLock struct {
db *sql.DB
}
func (l *AdvisoryLock) TryLock(ctx context.Context, key string, timeout time.Duration) (bool, error) {
lockID := hashKey(key)
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var acquired bool
err := l.db.QueryRowContext(ctx,
"SELECT pg_try_advisory_lock($1)", lockID).Scan(&acquired)
if err != nil {
return false, err
}
if acquired {
return true, nil
}
time.Sleep(50 * time.Millisecond)
}
return false, nil
}
func (l *AdvisoryLock) Unlock(ctx context.Context, key string) error {
lockID := hashKey(key)
_, err := l.db.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", lockID)
return err
}
func hashKey(key string) int64 {
h := fnv.New64a()
h.Write([]byte(key))
return int64(h.Sum64() & 0x7FFFFFFFFFFFFFFF)
}
Failure Scenarios
PostgreSQL Advisory Lock Failure
Scenario: Database connection dies while holding lock
1. Application acquires lock
2. Application crashes (or connection pool issue)
3. PostgreSQL detects disconnection
4. Lock automatically released
5. Another process can acquire lock
Result: Safe, automatic cleanup
Redlock Failure Scenarios
Scenario 1: Clock skew
1. Process A acquires lock (TTL 10s)
2. Clock on Redis instance 3 is 5s ahead
3. Lock on instance 3 expires early
4. Process B acquires majority including instance 3
5. Both A and B think they have the lock!
Scenario 2: Network partition
1. Process A acquires lock on 3/5 instances
2. Network partition isolates 2 instances
3. Process A's locks expire on isolated instances
4. When partition heals, state is inconsistent
Scenario 3: GC pause
1. Process A acquires lock (TTL 10s)
2. Process A has 8 second GC pause
3. Lock expires during GC
4. Process B acquires lock
5. Process A's GC ends, still thinks it has lock
Decision Matrix
Use PostgreSQL Advisory Locks When:
✅ You already have PostgreSQL
✅ Lock contention is low (<100 locks/sec)
✅ You need simplicity over performance
✅ Single database is acceptable (with standby)
✅ Session-based locking is acceptable
Use Redis/Redlock When:
✅ You need high throughput (1000+ locks/sec)
✅ You already have Redis infrastructure
✅ Lock TTL is critical (prevent stuck locks)
✅ You accept the complexity
✅ You've read Kleppmann's critique and accept risks
Use Neither - Use Fencing Tokens
For truly safe distributed locking:
1. Get lock + monotonic fencing token
2. Pass token to downstream systems
3. Downstream rejects if token < previously seen
This protects against all timing-related issues.
Monitoring
PostgreSQL Advisory Locks
-- View current locks
SELECT * FROM pg_locks WHERE locktype = 'advisory';
-- Count by session
SELECT pid, count(*)
FROM pg_locks
WHERE locktype = 'advisory'
GROUP BY pid;
Prometheus Metrics
# Lock acquisition time
histogram_quantile(0.99, rate(lock_acquisition_duration_seconds_bucket[5m]))
# Lock held duration
histogram_quantile(0.99, rate(lock_held_duration_seconds_bucket[5m]))
# Lock contention (waiters)
sum(lock_waiters_total)
Checklist
## Distributed Lock Selection
### Requirements Analysis
- [ ] Do you actually need distributed locks?
- [ ] Can you use database transactions instead?
- [ ] What is acceptable lock throughput?
- [ ] What happens if lock mechanism fails?
### PostgreSQL Advisory Locks
- [ ] Use session locks for long-running operations
- [ ] Use transaction locks for DB-bound operations
- [ ] Implement proper hash function for keys
- [ ] Add timeout for lock acquisition
### Redis/Redlock
- [ ] Deploy 5 independent Redis instances
- [ ] Use proper Redlock library
- [ ] Implement fencing tokens for safety
- [ ] Monitor clock skew between instances
### Monitoring
- [ ] Track lock acquisition latency
- [ ] Track lock hold duration
- [ ] Alert on lock contention
- [ ] Alert on lock timeouts
Conclusion
PostgreSQL advisory locks represent the kind of pragmatic solution that doesn’t get talked about at conferences. They’re not exciting. They don’t require new infrastructure. They don’t have fancy algorithms. But for most teams, they’re the right choice—simpler to operate, fewer moving parts, and actually safer than the alternatives.
The key insight is that operational complexity has real costs. Running five Redis instances for Redlock means monitoring five instances, managing failover, debugging split-brain scenarios, and training your team on Redis operations. If you already run PostgreSQL well, advisory locks add zero operational burden—they’re just another SQL query.
The safety story is also underappreciated. Redlock has known issues with clock skew and GC pauses. If one of your Redis instances has a clock that’s drifting, or if your application has a long garbage collection pause at the wrong moment, you can have two processes that both believe they hold the lock. PostgreSQL advisory locks don’t have these issues—the database is the single source of truth.
That said, there are legitimate reasons to use Redis: very high lock throughput (>1000/sec), or the need for lock TTL that’s decoupled from database connections. If you’re processing millions of events and need microsecond lock operations, PostgreSQL won’t keep up. Know your requirements.
Key principles:
- Simpler operations—one less service to manage, one less thing to break
- Good enough performance—850 ops/sec covers the vast majority of use cases
- Better safety—no clock dependency issues, no GC pause vulnerabilities
- Free—you already have PostgreSQL, advisory locks are built in
- Use Redis when you need it—genuine high throughput or TTL-based locks without database connections
Add Redis for locking only when you genuinely need capabilities PostgreSQL can’t provide. For most teams, that day never comes.
Related Articles
- Connection Pool Sizing with Little’s Law - Database connections
- PostgreSQL HOT Updates - PostgreSQL internals
Related posts
When Prepared Statements Make PostgreSQL 10× Slower: Generic Plan Trap
Same query, same params, but prod is slow and staging works fine. I'll show how to reproduce the generic plan problem with pgBouncer, Java/Go and how to fix it.
HTTP Keep-Alive Connection Reset: Why Your Requests Fail with 'Connection Reset by Peer'
Sporadic 'connection reset by peer' errors in production. I'll show how keep-alive timeout mismatches between client and server cause this and how to fix it.
Adaptive Concurrency Limits: Stop Guessing Thread Pool Sizes
Thread pool 200 because that's what Stack Overflow says? Netflix's algorithm adjusts concurrency automatically based on latency. I show how it works with benchmarks.
Kubernetes CPU Throttling Autopsy: Why p99 Latency Explodes at 40% CPU Usage
CPU looks OK but tail latency is catastrophic. I'll show how to correlate CFS throttling with latency spikes and why removing CPU limits can paradoxically help.
Cite this article
If you reference this post, please link to the original URL and credit the author.