DocumentationKV store for functions: persist state, dedupe and cache with TTL

KV store for functions: persist state, dedupe and cache with TTL

Give Webhook Relay functions a persistent key-value store with require("kv:name"): counters, webhook deduplication, cached API tokens and cursors with per-key TTL, in JavaScript or Lua. No database to run.

A Webhook Relay function can keep state between runs with require("kv:name"), a persistent key-value store with optional per-key TTL. Use it to deduplicate webhooks, cache an access token until it expires, count events or remember a sync cursor, with no database or Redis to run. The store is created automatically the first time a function imports it.

const kv = require("kv:sessions")

// Store any JSON value; the optional third argument is a TTL in seconds
kv.set("customer:42", { plan: "pro", seats: 5 }, 3600)

// Read it back (undefined when missing or expired)
const customer = kv.get("customer:42")

// Keep a counter across executions
const seen = (kv.get("events_seen") || 0) + 1
kv.set("events_seen", seen)

// Check, extend, list by prefix, delete
if (kv.has("customer:42")) {
  kv.setTtl("customer:42", 7200)
}
const page = kv.keys("customer:", 100) // { keys: [...], cursor: "..." }
kv.delete("customer:42")
local kv = require("kv:sessions")

-- Store any JSON value; the optional third argument is a TTL in seconds
kv.set("customer:42", { plan = "pro", seats = 5 }, 3600)

-- Read it back (nil when missing or expired)
local customer = kv.get("customer:42")

-- Keep a counter across executions
local seen = (kv.get("events_seen") or 0) + 1
kv.set("events_seen", seen)

-- Check, extend, list by prefix, delete
if kv.has("customer:42") then
  kv.setTtl("customer:42", 7200)
end
local page = kv.keys("customer:", 100) -- { keys = {...}, cursor = "..." }
kv.delete("customer:42")

How stores are created and shared

The name after kv: is the store. The first time a function runs require("kv:sessions"), Webhook Relay creates an empty KV store called sessions in your account and binds it to that function. Any other function that requires kv:sessions uses the same store, so two functions can share state (for example, one writes a cursor and another reads it).

Store names start with a letter, contain only letters, numbers, _ or -, and are at most 64 characters.

You can see and manage stores in the dashboard under Functions → KV: browse keys and values, edit or expire entries, clear a store, or delete it. Deleting a store works as a reset. The next run that requires the name gets a fresh, empty store.

API

All methods are synchronous: they return the result directly, with no await or callback.

MethodReturnsNotes
kv.get(key)value, or undefined (nil in Lua)Missing and expired keys both read as absent
kv.set(key, value[, ttl])truevalue is any JSON-serializable data. ttl in seconds; 0 or omitted means no expiry. A new set replaces any previous TTL
kv.has(key)boolean
kv.delete(key)true
kv.setTtl(key, ttl)trueChange the expiry without rewriting the value
kv.keys([prefix[, limit[, cursor]]]){ keys, cursor }Page through keys, optionally under a prefix. Pass the returned cursor to get the next page

Errors (an oversized value, a storage failure) are thrown as exceptions with a short message. Storage internals are never exposed to function code.

Deduplicate webhooks

Providers retry, and at-least-once delivery means your destination can see the same event twice. Use the event ID as the key and drop anything you have already seen:

const kv = require("kv:stripe_events")
const event = JSON.parse(r.body)

if (kv.has(event.id)) {
  r.stopForwarding()      // logged, not delivered again
  return
}
// Remember the ID for the provider's retry window (Stripe retries for up to 3 days)
kv.set(event.id, true, 7 * 24 * 3600)

The duplicate still appears in the bucket's logs, and the provider gets a normal success response, so it stops retrying. See filter or drop webhooks for other ways to stop a request.

Cache an access token

When a destination needs an OAuth token or a short-lived API key, fetch it once and cache it with a TTL a little shorter than its lifetime:

const kv = require("kv:tokens")

let token = kv.get("crm")
if (!token) {
  const res = http.request("POST", "https://auth.example.com/oauth/token", {
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: "grant_type=client_credentials&client_id=" + cfg.get("CLIENT_ID") + "&client_secret=" + cfg.get("CLIENT_SECRET"),
  })
  const body = JSON.parse(res.body)
  token = body.access_token
  kv.set("crm", token, body.expires_in - 60)   // refresh a minute early
}

r.setHeader("Authorization", "Bearer " + token)

The same pattern caches any slow lookup, such as enrichment data from an API you call from the function, so each webhook doesn't pay for the round trip.

Counters, rate limits and cursors

  • Counters. kv.set("count", (kv.get("count") || 0) + 1) keeps a running total across executions.
  • Simple rate limits. Key by a time window, for example "calls:" + Math.floor(Date.now() / 60000) with a 120-second TTL, and stop forwarding when the count passes your limit.
  • Cursors. Store the last processed ID or timestamp so a scheduled webhook that polls an API only fetches what's new.

KV operations are not atomic. A get followed by a set is two separate calls, so two concurrent executions can read the same value. That is fine for dedupe, caching and approximate counts. For a counter that must never race, serialize the work in one consumer.

Limits

LimitValue
Value size256 KiB (JSON-encoded)
Key size512 bytes
keys() page size100 by default, up to 1,000
Stores per account50 (KV and file stores combined)

For larger blobs, such as exports, generated reports or attachments, use the file store instead.

Manage stores from the API, MCP or an agent

Stores are also available through the REST API under /v1/function-stores (list, create, delete, and browse, set, expire or clear KV entries) and through the Webhook Relay MCP server, so a coding agent can inspect what a function has stored while it debugs it. The MCP tools include list_function_stores, list_function_kv_entries, get_function_kv_entry, set_function_kv_entry and clear_function_kv_store.

Migrating from Pipedream Data Stores

The API mirrors Pipedream's this.dataStore, so ports are mostly mechanical: this.dataStore.set(k, v, { ttl }) becomes kv.set(k, v, ttl), and you drop the await because calls are synchronous. The Pipedream Workflows migration guide has the full method-by-method mapping and a worked example.

Frequently asked questions

Does Webhook Relay have a key-value store for functions?

Yes. A function calls require("kv:name") and gets a persistent key-value store with get, set, has, delete, setTtl and keys. Values are any JSON-serializable data up to 256 KiB, and each key can have its own TTL in seconds. It works in both JavaScript and Lua functions.

Do I need to create the KV store before using it?

No. The first time a function runs require("kv:sessions"), Webhook Relay creates an empty store named sessions and binds it to that function. Other functions that require the same name share the same store. You can also create, browse and clear stores in the dashboard under Functions, KV.

Can I use the KV store as a cache with expiry?

Yes. Pass a TTL in seconds as the third argument to set, for example kv.set("token", value, 3600). After the TTL the key reads as missing, so the function fetches a fresh value and stores it again. setTtl changes the expiry of an existing key without rewriting its value.

How do I deduplicate webhooks with the KV store?

Use the provider's event or delivery ID as the key. If kv.has(id) is true, call r.stopForwarding() so the duplicate is logged but not delivered; otherwise store the ID with a TTL that covers the provider's retry window, such as seven days for Stripe, and let the request through.

Are KV operations atomic?

No. Each call is a single read or write, and a get followed by a set is not a transaction. Two concurrent executions can read the same old value. For a counter that must never race, serialize the work in one consumer instead of relying on the KV store.

What are the KV store limits?

A value can be up to 256 KiB once JSON-encoded and a key up to 512 bytes. keys() returns 100 keys per page by default and at most 1,000. An account can have up to 50 function stores in total, counting KV and file stores together.

Did this page help you?