Cache & Background Jobs
Bot-module handlers have two complementary APIs: ctx.cache for fast, Redis-backed storage per installation, and ctx.defer() for work that runs after the handler has returned its result.
Each API works on its own. You can use the cache without defer, defer without the cache, or combine them.
ctx.cache, ctx.defer(), ctx.fetch, ctx.secrets and ctx.db work in bot-module handlers. Handler server functions (query() / mutation() / action()) run on the same worker and have them too; their ctx.fetch refuses every host. Automation nodes do not have ctx.cache or ctx.defer() yet; there these calls do nothing.
ctx.cache - Per-install cache
A key-value store backed by Redis and scoped to the extension installation. Every key expires; the cache is for counters, cooldowns and short-lived state, not for permanent data.
API
ctx.cache = {
get: (key: string) => Promise<unknown | null>,
set: (key: string, value: unknown, ttl?: number) => Promise<void>,
delete: (key: string) => Promise<void>,
increment: (key: string, by?: number, ttl?: number) => Promise<number>,
};
Values are stored as JSON, so get returns what you stored with set (objects, arrays, numbers, strings).
Basic usage
import { command } from "@zaflun/lumio-sdk/server";
export const counter = command("count", async (ctx) => {
// Creates the key at 0 if it does not exist, then adds 1.
const count = await ctx.cache.increment("message_count");
return { reply: `Message count: ${count}` };
});
Setting values with TTL
// Store a value for 5 minutes
await ctx.cache.set("last_clipper", ctx.user.name, 300);
// Store a value with the default TTL (1 hour)
await ctx.cache.set("status", "active");
// Read it back: "active", or null once it has expired
const status = await ctx.cache.get("status");
set always applies the TTL you pass (or the default), also when it overwrites a key. increment sets the TTL only when it creates the key; later increments keep the original expiry.
Deleting values
await ctx.cache.delete("spam_score:user123");
Key scoping
Your extension only names the key. Lumio stores all keys of one installation in a single Redis hash, lumio:ext:cache:\{install_id\}, and your key is a field of that hash. A handler cannot address another installation's keys or any internal Lumio key. Uninstalling the extension deletes the installation's cache immediately.
Key format rules
Keys must match ^[A-Za-z0-9:_-]{1,128}$ (ASCII letters, digits, :, _, -). The check runs in Rust, outside your handler, so it cannot be bypassed. An invalid key throws.
Limits
| Limit | Value |
|---|---|
| Default TTL | 1 hour (3,600 seconds) |
| Max TTL | 24 hours (86,400 seconds) |
| Max key length | 128 characters |
| Max value size | 4 KB (serialized JSON) |
| Max keys per install | 256 |
| Cache calls per handler run | 50 |
| Cache operations per install | 200 per minute |
When a limit is reached the call throws, for example ctx.cache: cache is full (256 keys per install) or ctx.cache: rate limit exceeded. Writing to an existing key is still allowed when the cache is full. When Lumio's Redis is under memory pressure, set and increment throw ctx.cache is unavailable while reads keep working. Handle these errors if your module must keep running without the cache.
There are no wildcard operations: an extension cannot list its own keys.
When to use the cache
| Use case | ctx.cache |
|---|---|
| Cooldown counters | Yes |
| Spam scores | Yes |
| "Last chatter" queue | Yes |
| Temporary vote counts | Yes |
| Points that must survive longer than 24 hours | No: use ctx.db |
| Leaderboards, history, permanent user data | No: use ctx.db |
ctx.defer() - Background jobs
Queues a function that runs after the handler has returned. The bot posts the reply (or applies the moderation verdict) first; the deferred work runs afterwards in the same sandbox and does not count against the 500 ms deadline of command and moderation handlers.
API
ctx.defer(fn: () => Promise<void>): void
Basic usage
import { command } from "@zaflun/lumio-sdk/server";
export const points = command("points", async (ctx) => {
const key = `pts:${ctx.user?.id ?? "anon"}`;
const balance = ((await ctx.cache.get(key)) as number | null) ?? 0;
// Runs after the reply has been sent.
ctx.defer(async () => {
await ctx.fetch("https://api.example.com/points", {
method: "POST",
body: { user: ctx.user?.id, balance },
});
});
return { reply: `${ctx.user?.name}: ${balance} points` };
});
api.example.com must be in the manifest's egress.allowHosts.
Limits
| Limit | Value |
|---|---|
| Deferred jobs per handler run | 5 (the 6th ctx.defer() throws) |
| Time budget for all deferred jobs of one run | 10 seconds |
| Deferred jobs per install | 10 per minute |
| Available APIs | ctx.cache, ctx.fetch (same per-run call limits as the handler) |
| Runs when | Only after the handler finished without an error |
Jobs run one after another in the order you queued them. A job that throws does not stop the next one. When the per-minute limit is reached, the remaining jobs of that run are skipped. Deferred jobs are not persisted: if the worker restarts before they run, they are lost. Do not use them for work that must happen.
Error handling
A failing or timed-out deferred job never reaches the chat user; the reply has already been sent. The error is logged by the worker.
Combined pattern: reply fast, report later
import { command } from "@zaflun/lumio-sdk/server";
export const vote = command("vote", async (ctx, args) => {
const option = args[0] ?? "yes";
// Fast: one Redis round trip
const total = await ctx.cache.increment(`votes:${option}`, 1, 600);
// After the reply: push the tally to your own backend
ctx.defer(async () => {
await ctx.fetch("https://api.example.com/votes", {
method: "POST",
body: { option, total },
});
});
return { reply: `${option}: ${total} votes` };
});
Comparison
ctx.cache | ctx.defer() | |
|---|---|---|
| Backend | Redis (one hash per install) | Same sandbox, after the handler returned |
| Persistence | Until the TTL expires (max 24 h) | Not persisted |
| Latency | One Redis round trip | After the reply |
| Use case | Counters, cooldowns | Notifications, reporting |