# Ekx Upstash Redis

> Serverless Redis with Upstash — REST-based client for edge and serverless, rate limiting with @upstash/ratelimit, caching, and session or nonce storage. Use when adding rate limiting to an API route, caching an expensive read, or storing short-lived state that Supabase would be overkill for.

- Skill: `ekinoxis-evm/ekx-upstash-redis` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-upstash-redis`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-upstash-redis/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-upstash-redis

---


# Upstash Redis

Our serverless Redis, with `@upstash/ratelimit` on top of it.

Why Upstash and not a normal Redis: it speaks **HTTP**, so it works from Vercel edge
and serverless functions where a persistent TCP connection is not available. A regular
Redis client opens a connection per invocation and exhausts the connection pool under
load — that is the failure `ioredis` has to manage on a long-lived Railway process.

---

## Environment

```bash
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=       # SECRET
```

(`UPSTASH_REDIS_REST_KV_REST_API_URL`/`_TOKEN` also turn up — the names Vercel's KV
integration injects. Same values; prefer the plain pair.)

---

## Client

```ts
import { Redis } from "@upstash/redis";
export const redis = Redis.fromEnv();          // reads the two vars above
```

```ts
await redis.set("price:eth", 3200, { ex: 60 });   // TTL in seconds
const price = await redis.get<number>("price:eth");
```

**Always set a TTL.** Upstash bills per command and per stored byte; keys without
expiry accumulate silently and turn into a monthly line item nobody can attribute.

---

## Rate limiting

```ts
import { Ratelimit } from "@upstash/ratelimit";

const limiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "60 s"),
  analytics: true,
  prefix: "gt",
});

export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "anon";
  const { success, reset } = await limiter.limit(ip);
  if (!success) {
    return new Response("Demasiadas solicitudes", {
      status: 429,
      headers: { "Retry-After": String(Math.ceil((reset - Date.now()) / 1000)) },
    });
  }
  // …
}
```

Rate-limit by **authenticated user id where you have one**, falling back to IP.
IP alone is both too coarse (shared NAT in a school or office) and too easy to rotate.

Every route that costs money — an Anthropic call, a Pinata pin, an onchain write, an
email — should be behind a limiter. Those are the ones an attacker targets precisely
because they cost us more than they cost them.

---

## Caching pattern

```ts
async function cached<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {
  const hit = await redis.get<T>(key);
  if (hit !== null) return hit;
  const value = await fn();
  await redis.set(key, value, { ex: ttl });
  return value;
}
```

Good fits: token prices, Shopify catalog reads, Meta insights (which lag anyway), RPC
reads that change per block rather than per request.

---

## Gotchas

1. **No TTL** → unbounded storage and cost.
2. **Serialisation.** The SDK JSON-encodes automatically; do not `JSON.stringify` first or you get double-encoded strings back.
3. **`bigint` is not JSON-serialisable.** Store as string.
4. **Rate limiting by IP only** is both too coarse and too weak.
5. **Every command is billed**, including misses — a hot loop of cache lookups is not free.
6. **Not a database.** Anything that must survive is Supabase's job.

