Table of Contents

Workers KV

Workers KV is Cloudflare's global, low-latency key-value data store. This API allows you to manage KV namespaces and their key-value pairs programmatically.

Overview

public class KvService(ICloudflareApiClient cf)
{
    public async Task StoreConfigAsync(string namespaceId, string key, string value)
    {
        await cf.Accounts.Kv.WriteValueAsync(namespaceId, key, value);
    }

    public async Task<string?> GetConfigAsync(string namespaceId, string key)
    {
        return await cf.Accounts.Kv.GetValueAsync(namespaceId, key);
    }
}

API Limits

Limit Value
Key name max length 512 bytes
Value max size 25 MiB
Metadata max size 1024 bytes (serialized JSON)
Minimum TTL 60 seconds
Write rate limit 1 per second per key
Bulk write max items 10,000 pairs
Bulk write max size 100 MB total
Bulk delete max keys 10,000 keys
Bulk get max keys 100 keys

Namespace Operations

Creating a Namespace

var ns = await cf.Accounts.Kv.CreateAsync("my-config-store");

Console.WriteLine($"Created namespace: {ns.Id}");
Console.WriteLine($"Title: {ns.Title}");
Note

Namespace titles must be unique within your account. Creating a namespace with a duplicate title returns error code 10014.

Listing Namespaces

// List all namespaces automatically
await foreach (var ns in cf.Accounts.Kv.ListAllAsync())
{
    Console.WriteLine($"{ns.Title}: {ns.Id}");
}

// With ordering
var filters = new ListKvNamespacesFilters(
    Order: KvNamespaceOrderField.Title,
    Direction: ListOrderDirection.Ascending
);

await foreach (var ns in cf.Accounts.Kv.ListAllAsync(filters))
{
    Console.WriteLine(ns.Title);
}

Manual Pagination

var page = await cf.Accounts.Kv.ListAsync(new ListKvNamespacesFilters(
    Page: 1,
    PerPage: 50
));

Console.WriteLine($"Page {page.PageInfo.Page} of {page.PageInfo.TotalPages}");

foreach (var ns in page.Items)
{
    Console.WriteLine(ns.Title);
}

Getting a Namespace

var ns = await cf.Accounts.Kv.GetAsync(namespaceId);

Console.WriteLine($"Title: {ns.Title}");
Console.WriteLine($"Supports URL Encoding: {ns.SupportsUrlEncoding}");

Renaming a Namespace

var updated = await cf.Accounts.Kv.RenameAsync(namespaceId, "new-title");

Console.WriteLine($"Renamed to: {updated.Title}");

Deleting a Namespace

await cf.Accounts.Kv.DeleteAsync(namespaceId);
Warning

Deleting a namespace permanently removes all keys and values within it. This action cannot be undone.

Key-Value Operations

Writing Values

String Values

await cf.Accounts.Kv.WriteValueAsync(namespaceId, "config/app-name", "MyApp");

Binary Values

byte[] imageData = await File.ReadAllBytesAsync("logo.png");
await cf.Accounts.Kv.WriteValueAsync(namespaceId, "assets/logo", imageData);

With Expiration

// Expire at a specific Unix timestamp
await cf.Accounts.Kv.WriteValueAsync(
    namespaceId,
    "session/abc123",
    sessionData,
    new KvWriteOptions(Expiration: DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds())
);

// Expire after a TTL (in seconds, minimum 60)
await cf.Accounts.Kv.WriteValueAsync(
    namespaceId,
    "cache/user-profile",
    profileJson,
    new KvWriteOptions(ExpirationTtl: 3600) // 1 hour
);

With Metadata

Attach arbitrary JSON metadata to any key:

var metadata = JsonSerializer.SerializeToElement(new
{
    contentType = "application/json",
    version = 2,
    createdBy = "system"
});

await cf.Accounts.Kv.WriteValueAsync(
    namespaceId,
    "config/settings",
    settingsJson,
    new KvWriteOptions(Metadata: metadata)
);

Reading Values

String Values

string? value = await cf.Accounts.Kv.GetValueAsync(namespaceId, "config/app-name");

if (value is null)
{
    Console.WriteLine("Key not found");
}
else
{
    Console.WriteLine($"Value: {value}");
}

Binary Values

byte[]? data = await cf.Accounts.Kv.GetValueBytesAsync(namespaceId, "assets/logo");

if (data is not null)
{
    await File.WriteAllBytesAsync("downloaded-logo.png", data);
}

With Expiration Info (String)

var result = await cf.Accounts.Kv.GetValueWithExpirationAsync(namespaceId, "session/abc123");

if (result is not null)
{
    Console.WriteLine($"Value: {result.Value}");

    if (result.Expiration is not null)
    {
        var expiresAt = DateTimeOffset.FromUnixTimeSeconds(result.Expiration.Value);
        Console.WriteLine($"Expires: {expiresAt}");
    }
}

With Expiration Info (Binary)

var result = await cf.Accounts.Kv.GetValueBytesWithExpirationAsync(namespaceId, "assets/logo");

if (result is not null)
{
    await File.WriteAllBytesAsync("downloaded-logo.png", result.Value);

    if (result.Expiration is not null)
    {
        var expiresAt = DateTimeOffset.FromUnixTimeSeconds(result.Expiration.Value);
        Console.WriteLine($"Expires: {expiresAt}");
    }
}

Metadata Only

Read metadata without fetching the value (useful for large values):

var metadata = await cf.Accounts.Kv.GetMetadataAsync(namespaceId, "config/settings");

if (metadata is not null)
{
    var version = metadata.Value.GetProperty("version").GetInt32();
    Console.WriteLine($"Version: {version}");
}

Deleting Values

await cf.Accounts.Kv.DeleteValueAsync(namespaceId, "config/old-setting");
Tip

Delete operations are idempotent. Deleting a non-existent key does not throw an error.

Listing Keys

List All Keys

await foreach (var key in cf.Accounts.Kv.ListAllKeysAsync(namespaceId))
{
    Console.WriteLine($"Key: {key.Name}");

    if (key.Expiration is not null)
    {
        var expiresAt = DateTimeOffset.FromUnixTimeSeconds(key.Expiration.Value);
        Console.WriteLine($"  Expires: {expiresAt}");
    }

    if (key.Metadata is not null)
    {
        Console.WriteLine($"  Metadata: {key.Metadata}");
    }
}

Filter by Prefix

// List only keys under "config/"
await foreach (var key in cf.Accounts.Kv.ListAllKeysAsync(namespaceId, prefix: "config/"))
{
    Console.WriteLine(key.Name);
}

Manual Cursor Pagination

var page = await cf.Accounts.Kv.ListKeysAsync(namespaceId, new ListKvKeysFilters(
    Prefix: "users/",
    Limit: 100
));

foreach (var key in page.Items)
{
    Console.WriteLine(key.Name);
}

// Get next page using cursor
if (page.CursorInfo?.Cursor is not null)
{
    var nextPage = await cf.Accounts.Kv.ListKeysAsync(namespaceId, new ListKvKeysFilters(
        Prefix: "users/",
        Limit: 100,
        Cursor: page.CursorInfo.Cursor
    ));
}

Bulk Operations

Bulk Write

Write up to 10,000 key-value pairs in a single request:

var items = new[]
{
    new KvBulkWriteItem("config/setting1", "value1"),
    new KvBulkWriteItem("config/setting2", "value2"),
    new KvBulkWriteItem("config/setting3", "value3", ExpirationTtl: 3600),
};

var result = await cf.Accounts.Kv.BulkWriteAsync(namespaceId, items);

Console.WriteLine($"Successfully written: {result.SuccessfulKeyCount}");

if (result.UnsuccessfulKeys is { Count: > 0 })
{
    Console.WriteLine($"Failed keys: {string.Join(", ", result.UnsuccessfulKeys)}");
}

With Metadata

var metadata = JsonSerializer.SerializeToElement(new { source = "import" });

var items = new[]
{
    new KvBulkWriteItem(
        Key: "data/item1",
        Value: "value1",
        Metadata: metadata
    ),
    new KvBulkWriteItem(
        Key: "data/item2",
        Value: "value2",
        Metadata: metadata,
        ExpirationTtl: 86400 // 24 hours
    )
};

await cf.Accounts.Kv.BulkWriteAsync(namespaceId, items);

Bulk Delete

Delete up to 10,000 keys in a single request:

var keysToDelete = new[] { "old/key1", "old/key2", "old/key3" };

var result = await cf.Accounts.Kv.BulkDeleteAsync(namespaceId, keysToDelete);

Console.WriteLine($"Successfully deleted: {result.SuccessfulKeyCount}");

Bulk Get

Retrieve up to 100 values in a single request:

var keys = new[] { "config/a", "config/b", "config/c" };

var values = await cf.Accounts.Kv.BulkGetAsync(namespaceId, keys);

foreach (var (key, value) in values)
{
    if (value is not null)
    {
        Console.WriteLine($"{key}: {value}");
    }
    else
    {
        Console.WriteLine($"{key}: (not found)");
    }
}

With Metadata

var results = await cf.Accounts.Kv.BulkGetWithMetadataAsync(namespaceId, keys);

foreach (var (key, item) in results)
{
    if (item is not null)
    {
        Console.WriteLine($"{key}: {item.Value}");

        if (item.Metadata is not null)
        {
            Console.WriteLine($"  Metadata: {item.Metadata}");
        }
    }
}

Models Reference

KvNamespace

Property Type Description
Id string Unique namespace identifier
Title string Human-readable namespace name
SupportsUrlEncoding bool? Whether the namespace supports URL-encoded keys

KvKey

Property Type Description
Name string The key name
Expiration long? Unix timestamp when the key expires
Metadata JsonElement? Arbitrary JSON metadata attached to the key

KvWriteOptions

Property Type Description
Expiration long? Absolute Unix timestamp for expiration
ExpirationTtl int? Seconds until expiration (minimum 60)
Metadata JsonElement? Arbitrary JSON metadata (max 1024 bytes)

KvBulkWriteItem

Property Type Description
Key string The key name (max 512 bytes)
Value string The value to store
Expiration long? Absolute Unix timestamp for expiration
ExpirationTtl int? Seconds until expiration (minimum 60)
Metadata JsonElement? Arbitrary JSON metadata
Base64 bool? Set to true if value is base64-encoded binary

ListKvNamespacesFilters

Property Type Description
Page int? Page number (1-based)
PerPage int? Items per page
Order KvNamespaceOrderField? Field to order by (Id or Title)
Direction ListOrderDirection? Sort direction (Ascending or Descending)

ListKvKeysFilters

Property Type Description
Prefix string? Filter keys by prefix
Limit int? Maximum keys to return (default 1000)
Cursor string? Cursor for pagination

KvStringValueResult

Returned by GetValueWithExpirationAsync.

Property Type Description
Value string The value as a string
Expiration long? Unix timestamp when the key expires

KvValueResult

Returned by GetValueBytesWithExpirationAsync.

Property Type Description
Value byte[] The raw value bytes
Expiration long? Unix timestamp when the key expires

KvBulkWriteResult

Returned by BulkWriteAsync.

Property Type Description
SuccessfulKeyCount int Number of keys successfully written
UnsuccessfulKeys IReadOnlyList<string>? Keys that failed to write (should be retried)

KvBulkDeleteResult

Returned by BulkDeleteAsync.

Property Type Description
SuccessfulKeyCount int Number of keys successfully deleted
UnsuccessfulKeys IReadOnlyList<string>? Keys that failed to delete

KvBulkGetItemWithMetadata

Returned by BulkGetWithMetadataAsync as dictionary values.

Property Type Description
Value string? The value (null if key not found)
Metadata JsonElement? The metadata associated with the key

Common Patterns

Configuration Store

public class ConfigurationStore(ICloudflareApiClient cf, string namespaceId)
{
    public async Task<T?> GetAsync<T>(string key)
    {
        var json = await cf.Accounts.Kv.GetValueAsync(namespaceId, key);
        return json is null ? default : JsonSerializer.Deserialize<T>(json);
    }

    public async Task SetAsync<T>(string key, T value, int? ttlSeconds = null)
    {
        var json = JsonSerializer.Serialize(value);
        var options = ttlSeconds is not null
            ? new KvWriteOptions(ExpirationTtl: ttlSeconds)
            : null;

        await cf.Accounts.Kv.WriteValueAsync(namespaceId, key, json, options);
    }
}

Session Store

public class SessionStore(ICloudflareApiClient cf, string namespaceId)
{
    private const int SessionTtlSeconds = 3600; // 1 hour

    public async Task<string?> GetSessionAsync(string sessionId)
    {
        return await cf.Accounts.Kv.GetValueAsync(namespaceId, $"session/{sessionId}");
    }

    public async Task SetSessionAsync(string sessionId, string data)
    {
        await cf.Accounts.Kv.WriteValueAsync(
            namespaceId,
            $"session/{sessionId}",
            data,
            new KvWriteOptions(ExpirationTtl: SessionTtlSeconds)
        );
    }

    public async Task DeleteSessionAsync(string sessionId)
    {
        await cf.Accounts.Kv.DeleteValueAsync(namespaceId, $"session/{sessionId}");
    }
}

Bulk Import

public async Task ImportDataAsync(
    ICloudflareApiClient cf,
    string namespaceId,
    IEnumerable<KeyValuePair<string, string>> data)
{
    // Process in batches of 10,000
    var batches = data
        .Select(kv => new KvBulkWriteItem(kv.Key, kv.Value))
        .Chunk(10_000);

    foreach (var batch in batches)
    {
        var result = await cf.Accounts.Kv.BulkWriteAsync(namespaceId, batch);

        if (result.UnsuccessfulKeys is { Count: > 0 })
        {
            // Handle failures - retry or log
            Console.WriteLine($"Failed to write: {string.Join(", ", result.UnsuccessfulKeys)}");
        }
    }
}

Required Permissions

Permission Scope Level
Workers KV Storage Account Read (for listing and reading)
Workers KV Storage Account Write (for create, update, delete)