Service Discovery
Kahuna can store service instance metadata so clients and workers can discover healthy endpoints without running a separate registry.
Use a bucket per service:
services/{service-name}/{instance-id}
Examples:
services/payments/node-a
services/payments/node-b
services/search/node-c
Register an Instance
Use an expiration so the instance disappears if the process dies or stops refreshing its heartbeat.
set `services/payments/node-a` '{"host":"10.0.1.15","port":8080,"version":"1.2.3","zone":"us-east-1a"}' ex 30000 norev
From a client:
- .NET
- TypeScript
await client.SetKeyValue(
"services/payments/node-a",
"""{"host":"10.0.1.15","port":8080,"version":"1.2.3","zone":"us-east-1a"}""",
expiryTime: 30000,
flags: KeyValueFlags.SetNoRevision,
durability: KeyValueDurability.Persistent
);
await client.setNoRevision(
"services/payments/node-a",
'{"host":"10.0.1.15","port":8080,"version":"1.2.3","zone":"us-east-1a"}',
{
expiry: 30_000,
durability: "persistent"
}
);
Refresh the Heartbeat
Refresh the key periodically before the expiration passes:
- .NET
- TypeScript
await client.ExtendKeyValue(
"services/payments/node-a",
TimeSpan.FromSeconds(30),
KeyValueDurability.Persistent
);
await client.extend("services/payments/node-a", 30_000, {
durability: "persistent"
});
If instance metadata can change on heartbeat, write the full value again with the same expiration:
- .NET
- TypeScript
await client.SetKeyValue(
"services/payments/node-a",
currentMetadataJson,
expiryTime: 30000,
flags: KeyValueFlags.SetNoRevision,
durability: KeyValueDurability.Persistent
);
await client.setNoRevision("services/payments/node-a", currentMetadataJson, {
expiry: 30_000,
durability: "persistent"
});
Discover Instances
Read all instances in the service bucket:
get by bucket `services/payments`
From a client:
- .NET
- TypeScript
List<KahunaKeyValue> instances = await client.GetByBucket(
"services/payments",
KeyValueDurability.Persistent
);
foreach (KahunaKeyValue instance in instances)
Console.WriteLine($"{instance.Key}: {instance.ValueAsString()}");
const instances = await client.getByBucket("services/payments", {
durability: "persistent"
});
for (const instance of instances) {
console.log(`${instance.key}: ${instance.valueAsString()}`);
}
Audit Historical Membership
If you need to inspect which instances were visible at a previous point in time, use an AS OF read:
get by bucket `services/payments` as of 1718392012345
Or use a client snapshot parameter:
- .NET
- TypeScript
List<KahunaKeyValue> previous = await client.GetByBucket(
"services/payments",
KeyValueDurability.Persistent,
snapshotMs: 1718392012345
);
const previous = await client.getByBucket("services/payments", {
durability: "persistent",
snapshotMs: 1718392012345
});
Do not use NOREV for registrations when historical membership is required. No-revision writes keep only the latest value and skip the archived revision records needed by historical reads.
Choosing Durability
Use persistent durability when discovery state must survive node restarts and remain available during failover.
Use ephemeral durability when registrations are high-volume, short-lived, and safe to rebuild from running services.
eset `services/payments/node-a` '{"host":"10.0.1.15","port":8080}' ex 30000
eget by bucket `services/payments`
Operational Notes
- Set the expiration to several heartbeat intervals, not exactly one interval.
- Use no-revision writes for high-churn persistent registrations when only the current membership matters.
- Store enough metadata for clients to make routing decisions without another lookup.
- Keep service membership under a shared bucket so reads are consistent and partition-local.
- For large ordered registries, use key-range sharding instead of treating one bucket as permanently single-partition.