Skip to main content
KV supports server-side expiration (TTL): set a TTL on a write and the key is deleted automatically once it elapses. Without a TTL, a value lives until you delete it. KV has no per-key metadata.

Server-Side TTL

Pass expirationTtl on a binding put, a ttl_secs query parameter on a REST write, or --ttl on the CLI. The value is a whole number of seconds (19223372036); the key expires roughly that many seconds after the write, after which reads return null/404.
import { env } from "@telnyx/edge-runtime";

// Expire this key ~30 seconds after writing
await env.MY_KV.put("session/abc", JSON.stringify({ userId: 42 }), { expirationTtl: 30 });
An invalid ttl_secs (non-integer, 0, or negative) is rejected with 422 and the key is not written. The binding never produces that 422: it floors expirationTtl to a whole number of seconds and, if the result is less than 1, sends no TTL at all — the write succeeds and the key does not expire. There is no way to read the remaining TTL back — a get/list on a live key does not report its expiry.
expirationTtl requires @telnyx/edge-runtime ≥ 0.2.2. Earlier versions accept the option but silently ignore it — the key is written without a TTL.

No Per-Key Metadata

KV stores values as opaque bytes and has no per-key metadata. The binding’s put still accepts a metadata option so older code keeps compiling, but it is ignored (and marked @deprecated as of @telnyx/edge-runtime 0.2.2), and list never returns metadata. Anything you put in the value itself (including JSON that looks like those fields) is stored verbatim, not interpreted.

Application-Level Expiry

Server-side TTL deletes the key and tells you nothing else — there is no way to read a key’s remaining lifetime. Use this pattern instead when you want an absolute expires_at timestamp you can inspect on read (or when you’re pinned to @telnyx/edge-runtime < 0.2.2, where expirationTtl is ignored). Wrap your value with the timestamp and check it when you read; if it’s in the past, treat the key as missing (and optionally delete it).
import { env } from "@telnyx/edge-runtime";

async function putWithExpiry(key: string, value: string, ttlSeconds: number) {
    await env.MY_KV.put(key, JSON.stringify({
        value,
        expires_at: Date.now() + ttlSeconds * 1000,
    }));
}

async function getWithExpiry(key: string): Promise<string | null> {
    const wrapped = await env.MY_KV.get<{ value: string; expires_at: number }>(key, { type: "json" });
    if (wrapped === null) return null;              // key not found

    if (Date.now() > wrapped.expires_at) {
        await env.MY_KV.delete(key);                    // lazily clean up
        return null;                                 // expired
    }
    return wrapped.value;
}
// Usage: a session that "expires" after one hour
await putWithExpiry("session/abc", JSON.stringify({ userId: 42 }), 3600);

const session = await getWithExpiry("session/abc"); // null once an hour has passed
Using the REST API instead? You’d normally reach for ttl_secs above. Build this envelope on top of the REST API examples from the Quick Start only when you need the inspectable expires_at. Notes on this pattern:
  • Reads do the enforcing. An expired key still occupies storage until it’s read (and lazily deleted) or you delete it explicitly. Prefer native TTL (expirationTtl/ttl_secs) for eager server-side cleanup; if you do need a sweep, drive it from an external scheduler hitting your function over HTTP — HTTP is the only function trigger today.
  • Use a consistent clock. Date.now() on the edge node is fine for coarse expiry; don’t rely on it for sub-second precision.
  • Keep the envelope small. You pay for stored bytes, so the wrapper adds a little overhead per key.