# Key-Value Storage

Fast, ephemeral storage for caching, session data, and configuration

Key-value ("KV") storage provides fast data access for agents. Use it for caching, configuration, rate limiting, and data that needs quick lookups.

> [!TIP]
> **Not inside an agent or route?**
> Use the [`@agentuity/keyvalue`](/reference/standalone-packages#key-value-storage) standalone package to access this service from any Node.js or Bun app without the runtime.

## When to Use Key-Value Storage

| Storage Type | Best For |
|--------------|----------|
| **Key-Value** | Fast lookups, caching, configuration, rate limits |
| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG |
| [Object (S3)](/services/storage/object) | Files, images, documents, media |
| [Database](/services/database) | Structured data, complex queries, transactions |
| [Durable Streams](/services/storage/durable-streams) | Large exports, audit logs, real-time data |

> [!NOTE]
> **KV vs Built-in State**
> Use built-in state (`ctx.state`, `ctx.thread.state`, `ctx.session.state`) for data tied to active requests and conversations. Use KV when you need custom TTL, *persistent data across sessions*, or *shared state across agents*.

## Access Patterns

| Context | Access | Details |
|---------|--------|---------|
| Agents | `ctx.kv` | See examples below |
| Routes | `c.var.kv` | See [Using in Routes](#using-in-routes) |
| Standalone | `createAgentContext()` | See [Standalone Usage](#standalone-usage) |
| External backends | HTTP routes | [SDK Utilities for External Apps](/cookbook/patterns/server-utilities) |
| Frontend | Via routes | [React Hooks](/frontend/react-hooks) |

> [!NOTE]
> **Same API Everywhere**
> The KV API is identical in all contexts. `ctx.kv.get()` and `c.var.kv.get()` work the same way. See [Accessing Services](/reference/sdk-reference/router#accessing-services) for the full reference.

## Basic Operations

Access key-value storage through `ctx.kv` in agents or `c.var.kv` in routes. Buckets are auto-created on first use.

### Storing Data

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('CacheManager', {
  handler: async (ctx, input) => {
    // Store with custom TTL (minimum 60 seconds, maximum 365 days)
    await ctx.kv.set('cache', 'api-response', responseData, {
      ttl: 3600,  // expires in 1 hour
      contentType: 'application/json',
    });

    // Store with default TTL (7 days)
    await ctx.kv.set('cache', 'user-prefs', { theme: 'dark' });

    // Store with no expiration (persists indefinitely)
    await ctx.kv.set('config', 'feature-flags', {
      darkMode: true,
      betaFeatures: false,
    }, {
      ttl: null,  // never expires (0 also works)
    });

    return { success: true };
  },
});
```

### Retrieving Data

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('CacheRetriever', {
  handler: async (ctx, input) => {
    const result = await ctx.kv.get('cache', 'api-response');

    if (result.exists) {
      ctx.logger.info('Cache hit', {
        contentType: result.contentType,
        expiresAt: result.expiresAt,  // ISO timestamp when key expires (if TTL set)
      });
      return { data: result.data };
    }

    ctx.logger.info('Cache miss');
    return { data: null };
  },
});
```

### Deleting Data

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('SessionCleaner', {
  handler: async (ctx, input) => {
    await ctx.kv.delete('sessions', input.sessionId);
    return { deleted: true };
  },
});
```

## Type Safety

Use generics for type-safe data access:

```typescript
import { createAgent } from '@agentuity/runtime';
import { type } from 'arktype';

const UserPreferences = type({
  theme: "'light' | 'dark'",
  language: 'string',
  notifications: 'boolean',
});

type UserPreferences = typeof UserPreferences.infer;

const agent = createAgent('PreferenceLoader', {
  handler: async (ctx, input) => {
    const result = await ctx.kv.get<UserPreferences>('prefs', input.userId);

    if (result.exists) {
      // TypeScript knows the shape of result.data
      const theme = result.data.theme;  // Type: 'light' | 'dark'
      return { theme };
    }

    return { theme: 'light' };  // default
  },
});
```

## Additional Methods

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('StorageExplorer', {
  handler: async (ctx, input) => {
    // Search keys by keyword (returns keys with metadata)
    const matches = await ctx.kv.search('cache', 'user-');

    // List all keys in a namespace
    const keys = await ctx.kv.getKeys('cache');

    // List all namespaces
    const namespaces = await ctx.kv.getNamespaces();

    // Get statistics for a namespace
    const stats = await ctx.kv.getStats('cache');

    // Get statistics for all namespaces
    const allStats = await ctx.kv.getAllStats();

    return { keys, namespaces, stats, allStats };
  },
});
```

### Namespace Management

Create and delete namespaces programmatically:

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('NamespaceManager', {
  handler: async (ctx, input) => {
    // Create a namespace with default TTL for all keys
    await ctx.kv.createNamespace('cache', {
      defaultTTLSeconds: 3600,  // all keys expire in 1 hour by default
    });

    // Create a namespace with no expiration
    await ctx.kv.createNamespace('config', {
      defaultTTLSeconds: 0,  // keys never expire
    });

    // Delete a namespace (removes all keys)
    await ctx.kv.deleteNamespace('old-cache');

    return { success: true };
  },
});
```

**TTL semantics:**

| Value | Behavior |
|-------|----------|
| `undefined` | Keys expire after 7 days (default) |
| `null` or `0` | Keys never expire |
| `>= 60` | Custom TTL in seconds (minimum 60 seconds, maximum 365 days) |

> [!NOTE]
> **TTL Limits**
> The minimum TTL is 60 seconds. The maximum is 365 days (31,536,000 seconds). Values outside this range will be clamped to valid bounds. A TTL of 0 means the key never expires.

> [!NOTE]
> **Sliding Expiration**
> When a key is read with less than 50% of its TTL remaining, the expiration is automatically extended. This keeps frequently-accessed data alive without manual renewal.

> [!WARNING]
> **Destructive Operation**
> `deleteNamespace()` permanently removes the namespace and all its keys. This operation cannot be undone.

## TTL Strategy

Keys expire after 7 days by default unless a namespace-level or per-key TTL is set. Use TTL for temporary data:

| Data Type | Suggested TTL |
|-----------|---------------|
| API cache | 5-60 minutes (300-3600s) |
| Session data | 24-48 hours (86400-172800s) |
| Rate limit counters | Until period reset |
| Feature flags | No TTL (persistent) |

```typescript
import { createAgent } from '@agentuity/runtime';

interface UserSession {
  userId: string;
  email: string;
  loginAt: string;
  preferences: { theme: string };
}

const agent = createAgent('SessionManager', {
  handler: async (ctx, input) => {
    const sessionKey = `session:${input.token}`;

    // Check for existing session
    const existing = await ctx.kv.get<UserSession>('sessions', sessionKey);
    if (existing.exists) {
      return { session: existing.data };
    }

    // Create new session with 24-hour TTL
    const session: UserSession = {
      userId: input.userId,
      email: input.email,
      loginAt: new Date().toISOString(),
      preferences: { theme: 'light' },
    };

    await ctx.kv.set('sessions', sessionKey, session, {
      ttl: 86400,  // 24 hours
    });

    return { session };
  },
});
```

## Using in Routes

Routes have the same KV access via `c.var.kv`:

```typescript
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';

const router = new Hono<Env>();

router.get('/session/:id', async (c) => {
  const sessionId = c.req.param('id');
  const result = await c.var.kv.get('sessions', sessionId);

  if (!result.exists) {
    return c.json({ error: 'Session not found' }, 404);
  }

  return c.json({ session: result.data });
});

export default router;
```

> [!TIP]
> **External Backend Access**
> Need to access KV from a Next.js backend or other external service? Create authenticated routes that expose storage operations, then call them via HTTP. See [SDK Utilities for External Apps](/cookbook/patterns/server-utilities).

## Standalone Usage

Use KV storage outside of agent handlers with `createAgentContext()`:

```typescript
import { createApp, createAgentContext } from '@agentuity/runtime';

const app = await createApp();
export default app;

// Discord bot example
client.on('messageCreate', async (message) => {
  const ctx = createAgentContext();

  await ctx.invoke(async () => {
    // Check user preferences
    const prefs = await ctx.kv.get('prefs', message.author.id);

    if (!prefs.exists) {
      await ctx.kv.set('prefs', message.author.id, {
        theme: 'dark',
        notifications: true,
      });
    }
  });
});
```

See [Running Agents Without HTTP](/agents/standalone-execution) for more patterns including Discord bots, CLI tools, and queue workers.

## Best Practices

- **Use descriptive keys**: `user:{userId}:prefs` instead of `u123`
- **Set appropriate TTLs**: Prevent storage bloat with expiring cache entries
- **Handle missing keys**: Always check `result.exists` before accessing data
- **Keep values small**: KV is optimized for small-to-medium values; use Object Storage for large files

## Next Steps

- [Vector Storage](/services/storage/vector): Semantic search and embeddings
- [Object Storage (S3)](/services/storage/object): File and media storage
- [Database](/services/database): Relational data with queries and transactions
- [Durable Streams](/services/storage/durable-streams): Large data exports
- [State Management](/agents/state-management): Built-in request/thread/session state