AI Prompt Engineering

Getting ChatGPT to Write Accurate Distributed Lock Logic Without Split-Brain Bugs

July 08, 2026 8 min read 3 views

You paste a requirement into ChatGPT: "write a distributed lock using Redis". Within seconds you have something that looks production-ready. It compiles, the test passes, and you ship it. Six weeks later you wake up to a split-brain incident where two nodes simultaneously held the same lock and corrupted shared state. This is the failure mode ChatGPT doesn't warn you about unless you ask the right questions.

What you'll learn

  • Why naive distributed lock code from ChatGPT contains expiry-race and unsafe-release bugs by default.
  • Specific prompts that force ChatGPT to reason about fencing tokens and lock ownership.
  • How to evaluate the Redlock algorithm output ChatGPT produces and when to reject it.
  • A repeatable review checklist to apply to any distributed locking code an AI generates.

Prerequisites

You should be comfortable with Redis commands (SET NX PX, EVAL with Lua), basic distributed systems concepts (network partitions, process pauses), and have read or skimmed the original Martin Kleppmann / Antirez Redlock debate. If you haven't, even a quick skim will make everything in this article click faster.

Why distributed locks are a different class of problem

A local mutex protects memory inside a single process. A distributed lock must survive network delays, process pauses (GC, VM migration, OS scheduling), clock skew between nodes, and partial failures where a node crashes after acquiring the lock but before releasing it. Each of those failure modes can cause two processes to simultaneously believe they hold the lock — the split-brain condition.

This matters because ChatGPT was trained on a huge amount of blog-post-quality code, and most blog posts about Redis locks skip the hard parts. The model has absorbed that pattern and will reproduce it confidently unless you steer it differently. The same issue comes up in getting ChatGPT to write accurate mutex and locking code without deadlocks — the model defaults to the happy path.

The default ChatGPT output and its hidden flaws

Ask ChatGPT for a Redis distributed lock without any constraints and you will typically receive something like this Python snippet:

import redis
import uuid

client = redis.Redis()

def acquire_lock(lock_name: str, timeout: int = 10) -> str | None:
    identifier = str(uuid.uuid4())
    if client.set(lock_name, identifier, nx=True, ex=timeout):
        return identifier
    return None

def release_lock(lock_name: str, identifier: str) -> bool:
    current = client.get(lock_name)
    if current and current.decode() == identifier:
        client.delete(lock_name)  # BUG: non-atomic check-and-delete
        return True
    return False

There are at least two serious bugs here. First, the release_lock function does a read-then-delete in two separate commands. Between the GET and the DELETE, the lock can expire and be acquired by a different process. You then delete a lock you no longer own. Second, there is no fencing token, so even if the lock is released correctly, a process that paused after acquiring (GC stop-the-world, for example) can wake up and continue operating on a resource that another process now owns.

Prompting for the expiry-race condition

ChatGPT will not fix the check-and-delete race unless you name it explicitly. The following prompt reliably surfaces it:

"Rewrite the release_lock function so that the ownership check and the delete are atomic. Explain why a two-step GET + DELETE is unsafe under concurrent load and show me the Lua script that fixes it. Do not use any Redis client helper that abstracts the atomicity — show the raw EVAL call."

That prompt produces a correct Lua-based release:

RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
"""

def release_lock(lock_name: str, identifier: str) -> bool:
    result = client.eval(RELEASE_SCRIPT, 1, lock_name, identifier)
    return result == 1

The Lua script executes atomically inside Redis. There is no window between the ownership check and the delete. Always insist on this pattern. If ChatGPT produces a two-step release without explaining the race, it hasn't modelled the failure correctly.

Prompting for safe lock release with fencing tokens

Fixing the release atomicity still doesn't protect you from a process that pauses mid-operation. A process can acquire the lock, stall for longer than the TTL (GC, CPU scheduling, virtual machine migration), then wake up and write to shared storage after the lock has been re-granted to another process. This is the classic "process pause" split-brain scenario that Martin Kleppmann's fencing token paper addresses.

To force ChatGPT to reason about this, use a prompt that includes the failure scenario explicitly:

"Assume a process acquires the distributed lock with a 10-second TTL, then pauses for 15 seconds due to a GC stop-the-world event. When it wakes up, it believes it still holds the lock and writes to the database. Show me a fencing token approach that prevents the stale write from succeeding, and explain what storage-side support is required."

A good response will explain that the lock server should issue a monotonically increasing token on each successful acquisition, and the protected resource (database, file system, external API) must reject writes from a token lower than the highest token it has seen:

def acquire_lock_with_token(lock_name: str, timeout: int = 10):
    """Returns (identifier, fencing_token) or (None, None)."""
    identifier = str(uuid.uuid4())
    # Increment a separate counter atomically; only set the lock if not present
    pipe = client.pipeline(True)
    try:
        pipe.incr(f"{lock_name}:token")
        pipe.set(lock_name, identifier, nx=True, ex=timeout)
        token, acquired = pipe.execute()
        if acquired:
            return identifier, token
    except redis.WatchError:
        pass
    return None, None

The token increments regardless of whether the lock was acquired, so the sequence is strictly increasing across all attempts. The storage layer then checks: reject any write where fencing_token < max_seen_token. ChatGPT will produce this pattern when you frame the problem explicitly — but it almost never volunteers it unprompted. The same pattern of naming the failure scenario in the prompt is what makes getting ChatGPT to generate correct background job scheduler code reliable too.

Prompting for Redlock and why you should think twice

If you ask ChatGPT for a "production-grade" distributed lock, it will frequently suggest Redlock — the multi-node Redis algorithm. The code it produces is often structurally correct, but the algorithm itself remains controversial for use cases that require strict mutual exclusion under network partitions. Prompt ChatGPT to surface both sides:

"Implement Redlock in Python using 5 Redis nodes. Then describe the specific failure scenarios where Redlock can still allow two clients to hold the lock simultaneously, citing the clock-skew and process-pause arguments. Do not dismiss the criticism — explain when Redlock is acceptable and when it is not."

A well-guided response will implement Redlock and then note:

  • Redlock requires roughly synchronized clocks. If a node's system clock jumps forward (NTP correction, VM clock skew), a lock can expire earlier than expected on that node, opening a window for split-brain.
  • A process pause longer than the lock TTL breaks safety even with Redlock, because there is no fencing token baked into the algorithm.
  • For "efficiency" use cases (e.g., avoiding duplicate work) Redlock is fine. For "correctness" use cases (e.g., a single writer to a database record), a system with proper fencing token support (like ZooKeeper or etcd) is a safer choice.

If ChatGPT produces Redlock code without this nuance, add the phrase: "explain the Kleppmann critique and whether this implementation is safe for correctness-sensitive use cases." Being direct about the failure mode in your prompt is the fastest way to get an honest answer from the model.

Common pitfalls ChatGPT skips by default

Beyond the big two (unsafe release and missing fencing tokens), ChatGPT routinely omits several other details. Here is what to watch for and the follow-up prompt fragments that fix each one.

TTL set too short relative to critical section duration

ChatGPT picks a timeout value (10 seconds is common) without knowing how long your protected operation takes. If the operation can run longer than the TTL, the lock expires mid-operation. Ask explicitly: "How should I choose the lock TTL relative to my critical section duration, and what happens if the operation exceeds it?" The answer should include automatic lock extension (watchdog/heartbeat) or a hard upper bound enforced in the business logic.

No handling of Redis unavailability

If acquire_lock raises a ConnectionError, most generated code lets the exception propagate or silently returns None and the caller proceeds anyway. Prompt: "Show how the caller should handle the case where Redis is unreachable when trying to acquire the lock. Should the operation proceed, fail fast, or retry with backoff?" For correctness-sensitive operations the answer is almost always fail fast. For context on backoff patterns, see the guide on getting ChatGPT to write accurate backoff strategies without hammering downstream services.

Lock key namespace collisions

Generated code rarely includes a prefix strategy. If two different subsystems use client.set("job_lock", ...) they collide. Ask ChatGPT to show a namespacing convention: "Show a key naming convention for distributed locks that avoids collisions across services and environments." A pattern like lock:{service}:{env}:{resource_id} is easy to enforce and ChatGPT will produce it when asked.

Missing observability

Lock acquisition failures are silent in default generated code. You will not know if contention is high or if a runaway process is holding the lock indefinitely. Add: "Instrument acquire_lock and release_lock with structured log events that capture lock name, identifier, TTL, acquisition result, and contention count." This connects directly to what we cover in getting ChatGPT to write accurate logging middleware without swallowing errors.

Using the lock as a data consistency guarantee alone

ChatGPT sometimes generates code that relies entirely on the lock for data correctness without recommending a compensating database-level constraint. Prompt: "What database-level protections should exist even when a distributed lock is in place, assuming the lock can fail to prevent split-brain in extreme scenarios?" Optimistic locking (version columns, ETags) or conditional writes are the expected answer.

Wrapping up

ChatGPT can produce solid distributed lock code, but you have to name the failure modes explicitly in your prompts. Here are the concrete actions to take before you ship any AI-generated locking logic:

  1. Audit the release function. If it issues separate GET and DELETE commands, reject it immediately and prompt for the Lua-based atomic release.
  2. Ask about fencing tokens. If the lock protects writes to a shared resource, explicitly ask ChatGPT to explain the process-pause split-brain scenario and show fencing token support.
  3. Challenge any Redlock suggestion. Ask ChatGPT to explain the clock-skew and process-pause failure modes and confirm whether your use case is "efficiency" or "correctness" before committing to the algorithm.
  4. Verify TTL sizing and watchdog patterns. Make sure the TTL is either longer than your worst-case critical section or that a heartbeat extension mechanism is in place.
  5. Add observability and a fallback plan. Instrument every lock acquisition attempt and define what happens when Redis is unavailable — never let the caller silently proceed without a lock.

Frequently Asked Questions

Why does ChatGPT generate a non-atomic lock release and how do I fix it?

ChatGPT defaults to a two-step GET then DELETE pattern because most training examples use it. The fix is to run both operations inside a Lua script evaluated atomically by Redis, which eliminates the window between ownership check and deletion.

What is a fencing token and do I need one with a Redis distributed lock?

A fencing token is a monotonically increasing number issued on each lock acquisition and checked by the protected resource before accepting writes. You need one any time a process can pause longer than the lock TTL, because without it a stale process can write to a resource it no longer owns.

Is Redlock safe to use for correctness-critical distributed locking?

Redlock is considered safe enough for efficiency use cases like deduplicating work, but it is not recommended for strict correctness use cases because it lacks fencing token support and is vulnerable to clock skew and process pauses. ZooKeeper or etcd with proper fencing is the stronger choice for correctness-critical scenarios.

How should I choose the TTL for a distributed Redis lock?

The TTL should comfortably exceed your worst-case critical section duration, or you should implement a watchdog that extends the lock while the operation is still running. Setting it too short risks expiry mid-operation; setting it too long delays recovery if the holder crashes.

What should the caller do when Redis is unreachable and the lock cannot be acquired?

For correctness-sensitive operations the caller should fail fast and not proceed — operating without the lock risks data corruption. For lower-stakes efficiency locks, a short retry with exponential backoff may be appropriate, but Redis unavailability should always be logged as an alert-worthy event.

📤 Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

📬 Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.