Volter World

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

package.json
{
  "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.

.env.example
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
npm install --no-audit --no-fund

Create 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.

quota.mjs
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.mjs

Init 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 log

The 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 read

The 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 down

The 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.

View Markdown source

On this page