Redis su HTTP, fatturato per richiesta, senza pool di connessioni da accudire
Rate limit a finestra scorrevole in edge, token di idempotenza che sopravvivono ai retry, letture cache-aside con stale-while-revalidate. Colleghiamo Upstash dentro l'applicazione così la storia operativa combacia col deploy serverless: niente connessioni da gestire, niente capacità da pianificare, niente PagerDuty per Redis.
Perché questo stack
L'API HTTP significa che funziona in edge
Il Redis tradizionale ha bisogno di una connessione TCP. Le funzioni serverless e i runtime edge non tengono le connessioni TCP in modo pulito. Upstash espone Redis su HTTP/REST, così lo stesso codice che gira su Node gira anche in edge Cloudflare o Vercel senza cambi.
Paghi per richiesta, non per capacità provisionata
Un'istanza Redis normale fattura per dimensione del nodo e gira che tu la usi o no. Upstash fattura per numero di comandi. Per workload a burst (rate limiting, idempotenza, lookup di sessione) è la forma giusta. Modelliamo il costo in anticipo sui pattern di richiesta reali.
Libreria di rate limiting incorporata
`@upstash/ratelimit` arriva con sliding-window, token-bucket e fixed-window, tutti supportati da MULTI/EXEC di Redis. L'implementazione è corretta in concorrenza e l'API è una riga al call site.
Replicazione globale quando serve
I database globali Upstash replicano le scritture su più regioni e servono le letture dalla replica più vicina. Per un deploy multi-regione che ha bisogno della stessa view di rate limit e sessione ovunque, questo collassa un intero problema infrastrutturale in un flag di config.
Niente gestione delle connessioni è una feature
HTTP è stateless. Niente pool di connessioni da dimensionare, niente warm-up al cold start, niente connessione leakata che chiama qualcuno alle 3 di notte. La funzione serverless fa una richiesta HTTP e o riceve un risultato o un errore. Quella è tutta la complessità operativa.
Cosa sviluppiamo con questa tecnologia
Rate limiting per IP, per utente, per tenant
Sliding-window o token-bucket per caso, chiavi distinte per dimensione, fail-open o fail-closed configurati per route, header di risposta per X-RateLimit-Remaining.
Session storage con TTL
Sessioni basate su cookie storate su Redis con TTL esplicito, rotazione al cambio di privilegio, revoca server-side che si propaga in millisecondi.
Cache-aside per query lente
Un helper tipizzato `getOrSet` che legge da Redis, ricade sulla sorgente, riscrive con TTL, supporta stale-while-revalidate per resilienza.
Token di idempotenza
Una chiave unica per richiesta logica, registrata insieme alla risposta, replayata identica al retry. Combinata col pattern dei webhook Stripe, è il livello di dedupe production-grade.
Pattern di Edge Config
Feature flag, allowlist, kill switch letti da Redis in edge in millisecondi a una cifra. Niente servizio centrale da chiamare, niente JWT da validare.
Code per job a breve termine
Code basate su list per lavori brevi (invio email, trasformazione immagine, reindex di ricerca) con un worker che fa poll o un webhook che drena. Le code per lavori pesanti restano su un vero sistema di code.
Counter per analytics
Counter atomici per tenant, per route, per metrica. Aggregati in bucket temporali, flushati periodicamente al warehouse di analytics.
Primitiva di lock distribuito
Lock basati su SETNX con fencing token per sezioni critiche che non possono girare in concorrenza fra invocazioni di funzione.
Setup di replicazione multi-regione
Database globale configurato, regioni di lettura selezionate, regione di scrittura documentata, comportamento di fallback chiarito nel runbook.
Monitoring costi + alerting
Volume query giornaliero contro la quota, alert al 80 percento del budget, report di trend mensili collegati alla dashboard admin.
Migrazione da self-hosted o ElastiCache
Keyspace esistente esportato, replayato su Upstash, config applicativa scambiata, vecchio cluster dismesso con finestra di rollback.
Upstash Workflow per esecuzione durable
Workflow long-running retry-safe definiti come codice e orchestrati da Upstash. Usato dove un job in background ha più step che devono tutti completarsi prima o poi.
Rate limiting a finestra scorrevole e idempotenza in una sola Server Action
Un helper rate-limita la chiamata per IP e utente; un altro legge o scrive il record di idempotenza. La Server Action esegue entrambi prima di toccare il database, così una POST in retry colpisce il limiter una volta sola e la scrittura duplicata non avviene mai.
La maggior parte delle guide "Upstash quickstart" ti mostra una sola GET e SET. Il pattern che fa guadagnare a Upstash il suo posto in uno stack serverless è quello qui sotto: un rate limiter a finestra scorrevole e un check di token di idempotenza, entrambi che girano dentro una Server Action, entrambi supportati dalla stessa istanza Redis, con il pattern dei webhook Stripe visto prima nello stack che prende in prestito la stessa primitiva di idempotenza.
1. Il rate limiter
@upstash/ratelimit fa la correttezza algoritmica dentro Redis con MULTI/EXEC. Il codice applicativo è una riga al call site.
// src/lib/upstash/ratelimit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
export const ipLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(20, '10 s'),
prefix: 'rl:ip',
analytics: true,
})
export const userLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '60 s'),
prefix: 'rl:user',
analytics: true,
})
export const tenantLimiter = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(1000, '60 s', 1000),
prefix: 'rl:tenant',
analytics: true,
})
export async function checkRate(opts: {
ip: string
userId: string
tenantId: string
}): Promise<
| { ok: true }
| { ok: false; scope: 'ip' | 'user' | 'tenant'; reset: number }
> {
const ipResult = await ipLimiter.limit(opts.ip)
if (!ipResult.success) return { ok: false, scope: 'ip', reset: ipResult.reset }
const userResult = await userLimiter.limit(opts.userId)
if (!userResult.success) return { ok: false, scope: 'user', reset: userResult.reset }
const tenantResult = await tenantLimiter.limit(opts.tenantId)
if (!tenantResult.success) return { ok: false, scope: 'tenant', reset: tenantResult.reset }
return { ok: true }
}
2. L'helper di idempotenza
Una chiave unica codifica la forma della richiesta; la risposta è cachata contro quella chiave. Un retry con la stessa chiave ritorna la risposta originale senza rieseguire il lavoro.
// src/lib/upstash/idempotency.ts
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
const TTL_SECONDS = 24 * 60 * 60
interface RecordedResult<T> {
status: 'success' | 'error'
response: T
}
export async function withIdempotency<T>(
key: string,
run: () => Promise<T>,
): Promise<T> {
const fullKey = `idem:${key}`
const existing = await redis.get<RecordedResult<T>>(fullKey)
if (existing) {
if (existing.status === 'error') {
throw new Error('previous attempt failed; retry with a different key')
}
return existing.response
}
try {
const result = await run()
await redis.set(
fullKey,
{ status: 'success', response: result } satisfies RecordedResult<T>,
{ ex: TTL_SECONDS },
)
return result
} catch (err) {
await redis.set(
fullKey,
{ status: 'error', response: null } satisfies RecordedResult<unknown>,
{ ex: 60 },
)
throw err
}
}
3. La Server Action che usa entrambi
La Server Action prende un header Idempotency-Key, esegue il rate limiter, poi esegue la logica di business dentro l'helper di idempotenza. Una POST in retry con la stessa chiave ritorna la risposta originale; una scrittura duplicata non avviene mai.
// app/[lang]/(app)/invoices/actions.ts
'use server'
import { headers } from 'next/headers'
import { z } from 'zod'
import { checkRate } from '@/lib/upstash/ratelimit'
import { withIdempotency } from '@/lib/upstash/idempotency'
import { getServerSession } from '@/lib/auth/server'
import { stripe } from '@/lib/stripe/server'
const Input = z.object({
customerId: z.string(),
amountCents: z.number().int().positive(),
description: z.string().min(1).max(200),
})
export async function createInvoice(
raw: unknown,
): Promise<
| { ok: true; invoiceId: string }
| { ok: false; error: string; reset?: number }
> {
const session = await getServerSession()
if (!session) return { ok: false, error: 'unauthorised' }
const hdrs = await headers()
const ip = hdrs.get('x-forwarded-for')?.split(',')[0] ?? '0.0.0.0'
const idempotencyKey = hdrs.get('idempotency-key')
if (!idempotencyKey) return { ok: false, error: 'missing idempotency key' }
const rate = await checkRate({
ip,
userId: session.userId,
tenantId: session.tenantId,
})
if (!rate.ok) {
return { ok: false, error: `rate limited (${rate.scope})`, reset: rate.reset }
}
const parsed = Input.safeParse(raw)
if (!parsed.success) return { ok: false, error: 'invalid input' }
return withIdempotency(`invoice:${session.tenantId}:${idempotencyKey}`, async () => {
const invoice = await stripe.invoices.create({
customer: parsed.data.customerId,
collection_method: 'send_invoice',
days_until_due: 30,
description: parsed.data.description,
metadata: { tenant_id: session.tenantId },
})
await stripe.invoiceItems.create({
customer: parsed.data.customerId,
amount: parsed.data.amountCents,
currency: 'eur',
invoice: invoice.id,
})
return { ok: true as const, invoiceId: invoice.id }
})
}
4. Cosa ti compra questo
Il rate limiter gira in un paio di millisecondi in edge. L'helper di idempotenza trasforma una POST in retry in una lookup di cache, così l'API Stripe non vede mai un duplicato. Tutto il flusso vive in tre file; il livello Redis non compare nella logica applicativa se non come due helper che si leggono come funzioni normali.
Questo è l'Upstash che si guadagna il suo posto: non una curiosità "abbiamo sostituito Redis con HTTP", ma il livello operativo che rende i deploy serverless davvero sicuri sotto retry e traffico alto.
Domande frequenti
Upstash rispetto a ElastiCache o Memorystore?
Upstash per i deploy serverless, runtime edge e workload a burst dove il pricing pay-per-request è la forma giusta. ElastiCache o Memorystore quando hai un servizio Node long-running, un team infra interno e un workload dove la capacità provisionata batte la fatturazione per richiesta.
HTTP rispetto a TCP — quanta latenza in più?
Millisecondi a una cifra da una funzione Vercel o Cloudflare alla regione Upstash più vicina. Per il caso comune di rate-limit e lookup di sessione è più veloce del cold-start di una nuova connessione TCP. Misuriamo la latenza reale in staging.
La latenza in edge regge il confronto con un Redis regionale?
Per le letture dal database globale, sì — la lettura colpisce la replica più vicina. Per le scritture che devono fare round-trip alla regione primaria, accetti la latenza inter-regione. Documentiamo lo split lettura-vs-scrittura per caso d'uso.
Come scala il pricing a volume di richieste alto?
Linearmente. Upstash fattura per comando; traffico alto significa fattura più grande, prevedibile sulla curva. Per volumi di richieste molto alti un piano Pro con capacità riservata paga; lo modelliamo in fase di scoping basandoci sugli RPS attesi.
Possiamo eseguire script Lua su Upstash?
Sì. `EVAL` ed `EVALSHA` funzionano, e le operazioni atomiche complesse (le primitive di rate-limit stesse) viaggiano come script dentro le librerie ufficiali. Gli script custom sono supportati per la nostra logica applicativa dove conta il numero di round-trip.
E le garanzie di persistenza?
Upstash Redis persiste le scritture in modo durable con replicazione. Una read-after-write standard è consistente dentro la regione. Fra regioni, il database globale è eventualmente consistente con lag di replicazione in millisecondi bassi. Scegliamo il modello di consistenza per caso d'uso e lo documentiamo.
Come funziona la migrazione da Redis self-hosted?
Due strade. Per keyspace piccoli, dump e replay. Per quelli più grandi, dual-write per una finestra (l'applicazione scrive su entrambi, legge dal vecchio finché il catch-up non è verificato), poi cutover. Il runbook tiene aperta la strada del rollback finché non firmi.
Raccontaci cosa stai cachando, rate-limitando o coordinando
Una call di scoping, un numero concreto nella prima risposta, niente recite da agenzia. Integrazione Upstash in una settimana.