Skip to main content

Cache & Background Jobs

Bot modules (and all extension types) have access to two complementary APIs for performance optimization: ctx.cache for fast Redis-backed temporary storage, and ctx.defer() for queuing background work after the handler returns.

Each API works independently. You can use cache without defer, defer without cache, or combine them for patterns like cache-first reads with background DB persistence.

ctx.cache — Scoped Redis cache

A key-value cache backed by Redis, scoped to the extension installation. Provides sub-millisecond reads and writes without the overhead of a database round-trip.

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) => Promise<number>,
};

Basic usage

import { command } from "@zaflun/lumio-sdk/server";

export const counter = command("count", async (ctx) => {
// Increment a counter (creates the key with value 0 if it does not exist)
const count = await ctx.cache.increment("message_count", 1);
return { reply: `Message count: ${count}` };
});

Setting values with TTL

// Store a value with a 5-minute TTL
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
const status = await ctx.cache.get("status"); // "active" or null if expired

Deleting values

await ctx.cache.delete("spam_score:user123");

Key scoping

Your extension only provides the key name. The Rust Worker automatically prepends ext-cache:\{install_id\}: before executing any Redis command:

Extension calls: ctx.cache.set("score:user123", 42, 3600)
Redis executes: SET ext-cache:{install_id}:score:user123 42 EX 3600

You cannot access other extensions' cache keys or any internal Lumio Redis keys.

Key format rules

Keys must match the pattern ^[a-zA-Z0-9:_-]+$ — alphanumeric characters plus :, _, and -. This validation is enforced in Rust (not JavaScript), so it cannot be bypassed.

Limits

LimitValue
Max TTL24 hours (86,400 seconds)
Default TTL1 hour (3,600 seconds)
Max key length128 characters
Max value size64 KB
Max keys per install1,000
Operations rate limit200 ops/min per install
No-TTL behaviorKeys without explicit TTL get the 1-hour default

There are no wildcard operations (KEYS, SCAN). Extensions cannot enumerate their own keys.

When to use cache vs database

Use caseUse ctx.cacheUse ctx.db
Cooldown countersYesNo
Spam scoresYesNo
"Last chatter" queueYesNo
Temporary vote countsYesNo
User point balances (fast reads)Yes (with DB backup)Yes (source of truth)
LeaderboardsNoYes
Permanent user dataNoYes
Historical logsNoYes

ctx.defer() — Background jobs

Queues a function that the Worker executes after the handler returns. The handler can respond to the user immediately while expensive work runs in the background — outside the 500ms sync deadline.

API

ctx.defer(fn: () => Promise<void>): void

Basic usage

import { command } from "@zaflun/lumio-sdk/server";

export const points = command("points", async (ctx, args) => {
const balance = 100; // fast calculation

// Reply immediately
// Background work runs after this return
ctx.defer(async () => {
// This runs after the reply is sent
await ctx.db.insert("point_history", {
user_id: ctx.user.id,
action: "check",
timestamp: Date.now(),
});
});

return { reply: `${ctx.user.name}: ${balance} points` };
});

Limits

LimitValue
Max deferred calls per handler5
Timeout per deferred call10 seconds
Available APIsctx.db, ctx.cache, ctx.fetch, ctx.secrets
Error handlingLogged to Sentry, not surfaced to the chat user

Multiple deferred calls

You can queue up to 5 deferred calls per handler invocation. They execute sequentially after the handler returns:

export const gamble = command("gamble", async (ctx, args) => {
const result = Math.random() > 0.5 ? "win" : "lose";

ctx.defer(async () => {
await ctx.db.patch("user_points", ctx.user.id, {
balance: result === "win" ? 200 : 50,
});
});

ctx.defer(async () => {
await ctx.db.insert("gamble_history", {
user_id: ctx.user.id,
result,
timestamp: Date.now(),
});
});

return { reply: `${ctx.user.name} ${result === "win" ? "won" : "lost"}!` };
});

Error handling

If a deferred call throws an exception or times out, the error is logged but does not affect the user. The chat reply has already been sent. Errors from deferred calls appear in the developer dashboard error reports and lumio logs.


Combined pattern: cache-first with background persist

The most common pattern combines ctx.cache for fast reads with ctx.defer() for background database writes:

import { command } from "@zaflun/lumio-sdk/server";

export const points = command("points", async (ctx, args) => {
// Fast: read from cache (~1ms)
const cached = (await ctx.cache.get(`pts:${ctx.user.id}`)) as number | null;
const balance = cached ?? 0;
const newBalance = balance + 1;

// Fast: write to cache (~1ms)
await ctx.cache.set(`pts:${ctx.user.id}`, newBalance, 3600);

// Slow: persist to DB in background (~5-20ms, runs after reply)
ctx.defer(async () => {
await ctx.db.patch("user_points", ctx.user.id, { balance: newBalance });
await ctx.db.insert("point_history", {
user_id: ctx.user.id,
amount: 1,
timestamp: Date.now(),
});
});

return { reply: `${ctx.user.name}: ${newBalance} points` };
});

This pattern keeps the handler response under the 500ms sync deadline while ensuring data eventually reaches the database.

Comparison

ctx.cachectx.dbctx.defer()
BackendRedisPostgreSQLV8 (background)
PersistenceTemporary (TTL)PermanentN/A (orchestrator)
Latency~1ms~5-20msAsync (after return)
Use caseCounters, cooldownsUser data, leaderboardsBackground jobs