Skip to content

KV create() drops markerTTL on fallback path when DEL/PURGE marker exists #368

@Bre77

Description

@Bre77

Bug

KvBucket.create(k, data, markerTTL) silently drops the per-message TTL (Nats-Msg-TTL header) when the key has an existing DEL or PURGE marker.

Root Cause

create() in kv/src/kv.ts has two code paths:

  1. Happy path (line 592): this._put(k, data, { previousSeq: 0 }, markerTTL) — correctly forwards markerTTL, so the Nats-Msg-TTL header is set on the published message.

  2. Fallback path (line 611): When previousSeq: 0 fails because a DEL/PURGE marker exists, it calls this.update(k, data, rev)markerTTL is not forwarded. update() calls put() which calls _put() without the markerTTL argument, so no TTL header is set.

nats.js/kv/src/kv.ts

Lines 606 to 617 in d273721

let rev = 0;
try {
const e = await this.get(k);
if (e?.operation === "DEL" || e?.operation === "PURGE") {
rev = e !== null ? e.revision : 0;
return this.update(k, data, rev);
} else {
return Promise.reject(firstErr);
}
} catch (err) {
return Promise.reject(err);
}

Impact

Any caller using kv.create(key, data, ttl) for short-lived cache entries will lose the per-message TTL the second time a key is written (after the first entry expires and leaves a delete marker). The entry then persists until the bucket-level max_age instead of the intended TTL.

We observed this in production: cache entries written with a "14s" TTL were persisting for 7 days (the bucket max_age). Inspecting the stored message headers confirmed only Nats-Expected-Last-Subject-Sequence was present — no Nats-Msg-TTL.

Suggested Fix

Replace line 611:

return this.update(k, data, rev);

with:

return this._put(k, data, { previousSeq: rev }, markerTTL);

This calls _put() directly (same as the happy path) with previousSeq set to the revision of the existing delete/purge marker, and forwards markerTTL so the TTL header is set.

Reproduction

  1. Create a KV bucket with markerTTL enabled (to allow per-message TTL)
  2. kv.create("key", data, "10s") — succeeds, message has Nats-Msg-TTL: 10s header ✅
  3. Wait for TTL to expire (key is deleted, delete marker briefly exists)
  4. kv.create("key", data, "10s") during the delete marker window — succeeds, but message has no Nats-Msg-TTL header ❌
  5. Entry persists until bucket max_age instead of 10s

Environment

  • @nats-io/kv 3.3.0 and 3.3.1 (also present on main)
  • nats-server 2.11+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions