> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Key Expiration

> Telnyx KV supports server-side TTL — set it when writing through the binding, as a REST parameter, or with a CLI flag — so keys expire automatically.

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`.

<Tabs>
  <Tab title="Binding">
    ```ts theme={null}
    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 });
    ```
  </Tab>

  <Tab title="REST">
    ```bash theme={null}
    # 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}'
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    telnyx-edge storage kv key put <namespace-id> session/abc '{"userId": 42}' --ttl 30s
    ```
  </Tab>
</Tabs>

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.

<Note>
  `expirationTtl` requires `@telnyx/edge-runtime` **≥ 0.2.2**. Earlier versions accept the option but silently ignore it — the key is written without a TTL.
</Note>

## 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).

```ts theme={null}
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;
}
```

```ts theme={null}
// 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](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) 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.
