Compare-and-Set (CAS)
Two clients read the same key, both compute a new value, and both write it back. The second write silently erases the first: a lost update.
A compare-and-set (CAS) write prevents this. It succeeds only if the key still has the value or revision you expect, and fails if another client changed the key first, so your code can read again and retry. It is the same idea as UPDATE ... WHERE version = @expected in SQL.
Because the check against the current version and the write happen as one step, CAS guarantees strong consistency for that key: no writer can overwrite a change it has not seen.
| Use Case | How CAS Helps |
|---|---|
| Leader Election | Ensures only one node becomes leader. |
| Distributed Locks | Prevents multiple nodes from acquiring the same lock. |
| Configuration Updates | Prevents conflicting writes to shared config values. |
| Rate Limiting | Ensures atomic updates to request counters. |
| Concurrent Transactions | Avoids lost updates when multiple clients modify the same key. |
The leader election and lock uses above, and the queue consumer example below, rely on a key written with an expiry and nx as a simple lock. CAS does not stop a worker that paused past the expiry: it can still finish its work after another worker took over, and only its final CAS write fails. When work must not overlap, use distributed locks with fencing tokens.
Examples
In Kahuna Script, add cmp <value> or cmprev <revision> to set. The .NET and TypeScript clients have dedicated methods for CAS on a value or on a revision. Their signatures are in the API reference: Compare Value and Set and Compare Revision and Set.
Distributed Queue Consumer Lock
Use Case: Multiple workers process tasks from a queue but each task should only be handled once
- CLI
- .NET
- TypeScript
Sets the initial value as usual if it hasn't been assigned previously:
set `locks/tasks/123` "node1" ex 10000 nx
r0 set 15ms
Mark the task as completed if this node still holds the key:
set `locks/tasks/123` "completed" cmp "node1"
r1 set 11ms
using Kahuna.Client;
public async Task HandleTask(KahunaClient client, string taskId)
{
string keyName = "locks/task/" + taskId;
string machineName = Environment.MachineName;
// Sets the initial value as usual if it hasn't been assigned previously
KahunaKeyValue result = await client.SetKeyValue(
keyName,
machineName,
10000,
KeyValueFlags.SetIfNotExists
);
if (!result.Success)
{
Console.WriteLine("Other node acquired the task");
return;
}
// Process the task here
// ...
// Mark the task as completed if this node still holds the key
result = await client.TryCompareValueAndSetKeyValue(keyName, "completed", machineName, 0);
if (result.Success)
{
Console.WriteLine("Marked as completed successfully");
return;
}
}
async function handleTask(client, taskId, machineName) {
const keyName = "locks/task/" + taskId;
const acquired = await client.set(keyName, machineName, {
expiry: 10_000,
mode: "ifNotExists"
});
if (!acquired.success) {
console.log("Other node acquired the task");
return;
}
// Process the task here
// ...
const completed = await client.compareValueAndSet(
keyName,
"completed",
machineName
);
if (completed.success) {
console.log("Marked as completed successfully");
}
}
Feature Flag Update Control
Use Case: Update a feature flag, but only if it hasn’t changed since you last read it.
- CLI
- .NET
- TypeScript
Obtains the current value of the feature flag:
get `feature/new-landing`
r0 disabled 12ms
Change the flag if the revision is still 0:
set `feature/new-landing` "enabled" cmprev 0
r1 set 19ms
The flag won't change if the revision is not 0:
set `feature/new-landing` "disabled" cmprev 0
r1 not set 14ms
using Kahuna.Client;
public async Task CheckFeatureFlag(KahunaClient client, string featureName)
{
string featureKey = "feature/" + featureName;
// Obtains the current value of the feature flag
KahunaKeyValue current = await client.GetKeyValue(featureKey);
// ...
// Check if the flag was changed
KahunaKeyValue result = await client.TryCompareRevisionAndSetKeyValue(featureKey, "enabled", current.Revision);
if (result.Success)
{
Console.WriteLine("No one else changed the flag between our read and write.");
return;
}
}
async function checkFeatureFlag(client, featureName) {
const featureKey = "feature/" + featureName;
const current = await client.get(featureKey);
// ...
const updated = await client.compareRevisionAndSet(
featureKey,
"enabled",
current.revision
);
if (updated.success) {
console.log("No one else changed the flag between our read and write.");
}
}