Store data and apply a customer quota
Use the real Upstash Redis and rate-limit SDKs to store a customer and allow two requests per minute. Inspect the refused third request, stop the World, resume the same exhausted budget and advance its clock into the next window. No Upstash or Volter platform account is needed.
Use Node 22.6 or newer, npm and an empty app directory. Installation uses the normal
package registry. This recipe selects admitted @volter/twin-upstash@1.0.2 and pins
its exercised SDK and World versions. Keep those pins and the generated lockfile.
Keep the example's twin and SDK pins together. Upstash twin 1.0.2 supports the
script flags used by rate-limit SDK 2.2.0; earlier twin releases do not.
Use the release selection guide
before upgrading an existing World.
Prepare the app
{
"name": "world-quota-user",
"private": true,
"type": "module",
"dependencies": {
"@upstash/ratelimit": "2.2.0",
"@upstash/redis": "1.39.0"
},
"devDependencies": {
"@volter/twin-upstash": "1.0.2",
"@volter/world": "3.0.158",
"@volter/world-console": "3.0.149",
"@volter/world-core": "3.0.148",
"@volter/world-runtime": "3.0.156"
}
}Declare the environment names your app reads. Leave the values empty: the World issues a throwaway database token and supplies its endpoint when it starts.
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=npm install --no-audit --no-fundCreate quota.mjs. This chooses a fixed window, one budget per customer ID and a
separate key prefix. It disables the SDK's process-local cache and analytics, and
sets timeout: 0 so a Redis failure cannot become a timeout-based allowed request.
The pending promise is awaited before the app exits.
import { Redis } from '@upstash/redis';
import { Ratelimit } from '@upstash/ratelimit';
const redis = Redis.fromEnv();
const budget = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(2, '1 m'),
prefix: 'world-user-budget',
ephemeralCache: false,
analytics: false,
timeout: 0,
});
const customer = 'customer-1001';
if (process.argv[2] === 'read') {
console.log(JSON.stringify({
customer: await redis.get('app:customer'),
budget: await budget.getRemaining(customer),
}, null, 2));
} else {
await redis.set('app:customer', { id: customer, name: 'Ada' });
for (let request = 1; request <= 3; request++) {
const decision = await budget.limit(customer);
await decision.pending;
console.log(JSON.stringify({
request,
decision: decision.success ? 'allow' : 'deny',
limit: decision.limit,
remaining: decision.remaining,
reset: decision.reset,
reason: decision.reason,
}));
}
}Consume the budget
./node_modules/.bin/volter world init --name quota-user --twins upstash
./node_modules/.bin/volter world up
./node_modules/.bin/volter world clock set 2026-01-15T12:00:00Z
./node_modules/.bin/volter world run -- node quota.mjsInit selects only Upstash. Its endpoint is injected and the token is issued at
boot; the SDK keeps Redis.fromEnv().
The app prints allow with one request remaining, allow with zero remaining,
and deny with zero remaining. The limit is two and the reset is the end of the
frozen minute. A decision is your application's cue to continue or refuse work;
this small client does not serve an HTTP endpoint or emit a 429 response itself.
./node_modules/.bin/volter world logThe log shows the customer value, Redis counters, expiry and cached scripts. These are stored SDK operations. A denied request may still update the limiter's counter; a refusal does not mean nothing was stored.
Resume and open the next window
./node_modules/.bin/volter world down
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node quota.mjs readThe read client returns Ada and zero remaining requests. Down retained the database and frozen clock. It did not reset the budget or seed another customer.
./node_modules/.bin/volter world clock advance 1m
./node_modules/.bin/volter world run -- node quota.mjs read
./node_modules/.bin/volter world run -- node quota.mjs
./node_modules/.bin/volter world downThe read now shows two remaining requests and a reset one minute later. The next
three requests again produce allow, allow, deny. No wall-clock wait is needed.
Reset discards the current branch's state; use it deliberately to replay from default data.
This walk covers Redis SET/GET, a per-customer fixed-window limit, refusal, retained readback and clock-driven renewal. It does not establish sliding windows, analytics, traffic protection, dynamic limits, multiple regions or QStash delivery. Consult the selected Upstash release before using another operation.
Continue with your existing app, inspecting stored changes or recovering a failed call.