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 (1–9223372036); 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 });
# Expire this key ~30 seconds after writing
curl -X PUT "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/session%2Fabc?ttl_secs=30" \
-H "Authorization: Bearer $TELNYX_API_KEY" \
--data-binary '{"userId": 42}'
telnyx-edge storage kv key put <namespace-id> session/abc '{"userId": 42}' --ttl 30s
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.
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.