API Rate Limiting with Upstash Redis: 2026 Node Tutorial
May 24, 2026
API rate limiting caps how many requests each caller can make in a time window, shielding your service from floods, scrapers, and runaway bills. This tutorial builds production-grade rate limiting for a Node.js and Hono API using Upstash Redis sliding windows, in about 30 minutes.
TL;DR
This hands-on guide adds real rate limiting to a Node.js API instead of hoping nobody abuses it. You will create an Upstash Redis database, build a sliding-window limiter with @upstash/ratelimit, identify callers by a trusted IP (the leftmost X-Forwarded-For value is a security trap), package everything as reusable Hono middleware, return spec-correct 429 responses with Retry-After and rate-limit headers, give anonymous, free, and paid callers different limits, and harden the whole thing against floods. It uses Node.js 24 LTS, hono 4.12.22, and @upstash/ratelimit 2.0.81. Every file is copy-paste runnable and was type-checked against the published packages on 24 May 2026.
What you'll learn
- Create an Upstash Redis database and connect it to a Node.js service
- Scaffold a typed Hono API worth protecting
- Build a sliding-window rate limiter with
@upstash/ratelimit— and why a sliding window beats a fixed window - Identify callers by a trusted client IP, and why the leftmost
X-Forwarded-Forvalue is spoofable - Resolve a caller's tier so anonymous, free, and paid users get different limits
- Return spec-correct
429 Too Many Requestsresponses withRetry-Afterand rate-limit headers - Package rate limiting as reusable, typed Hono middleware
- Harden against floods with an ephemeral cache, a fail-open timeout, and deny lists
Prerequisites
- Node.js 24 LTS or newer. Node.js 24 is the current Active LTS line, supported until April 20282. Check with
node --version. - An Upstash account. The free tier is enough for this tutorial and small production workloads.
- Working knowledge of TypeScript and HTTP request/response basics.
- A terminal with
curlfor the verification step.
The stack: Hono 4.12.22 as the web framework, @hono/node-server 2.0.4 as the Node adapter, and @upstash/ratelimit 2.0.8 with @upstash/redis 1.38.0 for the limiter.
Step 1 — Create an Upstash Redis database
Rate limiting needs a fast, shared counter store. A sliding-window limiter increments a counter on every request, so the store must handle low-latency reads and writes and expire keys automatically — exactly what Redis is built for. Upstash gives you a serverless Redis accessed over HTTP, which means the same limiter code runs on a long-lived Node server, a container, or an edge function without connection-pool juggling.
Sign in at console.upstash.com, open the Redis section, and click Create Database. Pick a name, choose a primary region close to where your API runs, and create it. On the database page, find the REST API section and copy two values: UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.
The free tier currently includes 256 MB of storage, 500,000 commands per month, and one database per account; beyond that, pay-as-you-go billing is 0.20 USD per 100,000 commands, as of May 20263. A sliding-window check costs three to five Redis commands depending on state, so budget accordingly — Step 8 cuts that cost sharply.
Create a project folder and store the credentials in a .env file:
mkdir rate-limited-api && cd rate-limited-api
cat > .env <<'EOF'
UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-rest-token
PORT=3000
TRUSTED_PROXY_HOPS=0
EOF
echo ".env" > .gitignore
Never commit .env — the REST token is a full credential for your database. The .gitignore line above keeps it out of version control.
Step 2 — Scaffold the Hono API
Now build a small API worth protecting. Initialize the project and install pinned dependencies:
npm init -y
npm pkg set type=module
npm install hono@4.12.22 @hono/node-server@2.0.4 \
@upstash/ratelimit@2.0.8 @upstash/redis@1.38.0
npm install -D typescript@6.0.3 tsx@4.22.3 @types/node@25.9.1
Add a tsconfig.json configured for modern Node ES modules:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "nodenext",
"lib": ["ES2023"],
"types": ["node"],
"strict": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
noUncheckedIndexedAccess is on deliberately: rate-limiting code parses untrusted header strings, and having TypeScript force you to handle undefined from every array index is a real safety net.
Replace the scripts block in package.json:
{
"scripts": {
"dev": "node --env-file=.env --import tsx src/index.ts",
"build": "tsc",
"start": "node --env-file=.env dist/index.js"
}
}
Node 24 reads .env natively with --env-file, so there is no dotenv dependency. Create src/env.ts to validate the variables once at startup instead of discovering a missing token mid-request:
function required(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
export const env = {
UPSTASH_REDIS_REST_URL: required('UPSTASH_REDIS_REST_URL'),
UPSTASH_REDIS_REST_TOKEN: required('UPSTASH_REDIS_REST_TOKEN'),
PORT: Number(process.env.PORT ?? 3000),
TRUSTED_PROXY_HOPS: Number(process.env.TRUSTED_PROXY_HOPS ?? 0),
};
Create a minimal API in src/index.ts — one liveness route and one endpoint to protect:
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { env } from './env.js';
const app = new Hono();
app.get('/health', (c) => c.json({ status: 'ok' }));
app.get('/api/quote', (c) =>
c.json({ quote: 'Premature optimization is the root of all evil.' }),
);
serve({ fetch: app.fetch, port: env.PORT }, (info) => {
console.log(`API listening on http://localhost:${info.port}`);
});
Run npm run dev and confirm curl http://localhost:3000/api/quote returns the JSON quote. Right now /api/quote will answer a million requests a second without complaint. That is the problem the rest of this tutorial fixes.
Step 3 — Build a sliding-window rate limiter
Before wiring anything in, pick the algorithm. A fixed window counts requests in fixed clock slices — say, 00:00:00 to 00:00:10. The flaw is the boundary: a caller can send a full window of requests at 00:00:09 and another full window at 00:00:11, landing double the intended rate in two seconds4.
A sliding window fixes this. It still divides time into slices, but it weights the previous slice by how much of it still overlaps the current moment. If you allow 10 requests per minute and you are 15 seconds into the current window with 4 requests in the previous window and 5 so far in this one, the limiter estimates 4 * ((60 - 15) / 60) + 5 = 8 and lets the request through4. The boundary burst is smoothed away, at the cost of also reading the previous window's counter on each check. For almost every API, that trade is worth it.
Create src/ratelimit.ts. It defines three tiers up front and builds one limiter per tier:
import { Ratelimit, type Duration } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { env } from './env.js';
const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
});
interface TierConfig {
limit: number;
window: Duration;
windowSeconds: number;
}
// One source of truth for every tier's limits.
export const TIERS = {
anon: { limit: 20, window: '10 s', windowSeconds: 10 },
free: { limit: 100, window: '60 s', windowSeconds: 60 },
paid: { limit: 1000, window: '60 s', windowSeconds: 60 },
} satisfies Record<string, TierConfig>;
export type Tier = keyof typeof TIERS;
function buildLimiter(tier: Tier): Ratelimit {
const cfg = TIERS[tier];
return new Ratelimit({
redis,
prefix: `rl:${tier}`,
limiter: Ratelimit.slidingWindow(cfg.limit, cfg.window),
analytics: true,
});
}
export const limiters: Record<Tier, Ratelimit> = {
anon: buildLimiter('anon'),
free: buildLimiter('free'),
paid: buildLimiter('paid'),
};
Three things matter here. The prefix namespaces each tier's keys in Redis so the limiters never collide. Ratelimit.slidingWindow(limit, window) takes a request count and a duration string like '10 s' or '60 s'. And analytics: true records every check so you can see allowed and blocked counts in the Upstash Console's Rate Limit dashboard — it adds one Redis command per call5.
Using a limiter is a single call. limiters.anon.limit(identifier) returns a promise that resolves to an object you can act on:
const { success, limit, remaining, reset, reason } =
await limiters.anon.limit('ip:203.0.113.7');
// success: false if the caller exceeded the limit
// limit: max requests in the window (20 for anon)
// remaining: requests left in the current window
// reset: Unix timestamp in MILLISECONDS when the window resets
// reason: 'timeout' | 'cacheBlock' | 'denyList' | undefined
reset is a millisecond timestamp. For a sliding window it points at the start of the next window rather than an exact per-caller reset, which is close enough for a Retry-After value4. The identifier is whatever string you decide represents "one caller" — and choosing it correctly is its own problem.
Step 4 — Identify callers without trusting spoofable headers
A rate limiter is only as good as its identifier. If every request maps to the same key, you have built a global throttle; if an attacker can pick their own key, you have built nothing.
For anonymous traffic the natural key is the client IP. The naive approach reads the leftmost value of the X-Forwarded-For header — and it is a genuine security hole. X-Forwarded-For is a comma-separated list that each proxy appends to. The leftmost entries are supplied by the client and completely unverified, so an attacker can send a different fake IP on every request and sail past a limiter that keys on them6.
The only IP you can trust is the one your own infrastructure added: the TCP socket peer address, or — when you run behind reverse proxies you control — the entry those proxies appended, counted from the right6. Create src/client-ip.ts:
import type { Context } from 'hono';
import { getConnInfo } from '@hono/node-server/conninfo';
import { env } from './env.js';
/**
* Resolve the client IP we can trust for rate limiting.
*
* The TCP socket peer address cannot be spoofed by the client. Behind
* reverse proxies you control, those proxies append the real client IP
* to X-Forwarded-For, so we count in from the RIGHT. The leftmost
* entries are attacker-controlled and must never key a limiter.
*/
export function getClientIp(c: Context): string {
const socketIp = getConnInfo(c).remote.address ?? 'unknown';
if (env.TRUSTED_PROXY_HOPS === 0) {
return socketIp;
}
const forwarded = c.req.header('x-forwarded-for');
if (!forwarded) {
return socketIp;
}
const chain = forwarded
.split(',')
.map((part) => part.trim())
.filter(Boolean);
// Each trusted proxy appends one entry, so the rightmost N entries are
// from your own infrastructure. The real client IP is the leftmost of
// those N — at index (length - N).
const index = chain.length - env.TRUSTED_PROXY_HOPS;
return chain[index] ?? socketIp;
}
getConnInfo comes from the Hono Node adapter and returns the real socket peer address7. When the app is directly internet-facing, set TRUSTED_PROXY_HOPS=0 and use that address. When you add one proxy — an Nginx instance, a cloud load balancer — set it to 1; for a CDN in front of a load balancer, set it to 2. The value must match your real deployment exactly: set it too high and you start trusting attacker-supplied entries again.
Step 5 — Resolve the caller's tier
Anonymous callers share an IP-based limit, but authenticated callers should be identified by their API key. Keying paid callers on their key instead of their IP also means one customer is never throttled because a noisy neighbour shares their office IP.
Create src/tiers.ts to map an API key to a tier:
import { type Tier } from './ratelimit.js';
// Demo key store. In production, look API keys up in your database
// (and cache the lookup) instead of hard-coding them.
const API_KEYS = new Map<string, Tier>([
['demo-free-key', 'free'],
['demo-paid-key', 'paid'],
]);
/** Map an incoming API key to a tier. No key, or an unknown key, is anonymous. */
export function resolveTier(apiKey: string | undefined): Tier {
if (apiKey) {
const tier = API_KEYS.get(apiKey);
if (tier) return tier;
}
return 'anon';
}
The rule is deliberately strict: a missing key or an unrecognized key both fall through to anon. A caller cannot promote themselves to the paid tier by inventing a key, because an unknown key resolves to the lowest tier, not an error. In a real service, replace the hard-coded Map with a database lookup — and cache it, because this runs on every request.
Step 6 — Build spec-correct 429 responses
When a caller is over the limit, the response matters as much as the rejection. The correct status is 429 Too Many Requests8, and a well-behaved API tells the client how long to wait with a Retry-After header, whose value can be a count of seconds9. Clients and SDKs use it to back off instead of hammering you harder.
It also helps to advertise the limit on every response, not just rejections, so clients can self-throttle before they hit the wall. Two header conventions exist. The de-facto triple — X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset — is what GitHub and most APIs send today. The IETF is standardizing a replacement: draft-ietf-httpapi-ratelimit-headers defines structured RateLimit and RateLimit-Policy fields. It is still an Internet-Draft — the latest revision is dated April 2026 and it is not yet a finalized RFC10 — so the pragmatic move is to send both.
Create src/rate-limit-headers.ts:
import type { Context } from 'hono';
import { TIERS, type Tier } from './ratelimit.js';
/** The subset of an @upstash/ratelimit limit() response we need for headers. */
export interface RateLimitResult {
success: boolean;
limit: number;
remaining: number;
reset: number;
reason?: 'timeout' | 'cacheBlock' | 'denyList';
}
/**
* Attach rate-limit headers to the response and return the Retry-After
* value (seconds until the window resets).
*/
export function applyRateLimitHeaders(
c: Context,
tier: Tier,
result: RateLimitResult,
): number {
const retryAfter = Math.max(0, Math.ceil((result.reset - Date.now()) / 1000));
const safeRemaining = Math.max(0, result.remaining);
const policy = TIERS[tier];
// De-facto headers most HTTP clients and SDKs already understand.
c.header('X-RateLimit-Limit', String(result.limit));
c.header('X-RateLimit-Remaining', String(safeRemaining));
c.header('X-RateLimit-Reset', String(Math.ceil(result.reset / 1000)));
// Emerging IETF structured fields (draft-ietf-httpapi-ratelimit-headers).
c.header('RateLimit-Policy', `"${tier}";q=${policy.limit};w=${policy.windowSeconds}`);
c.header('RateLimit', `"${tier}";r=${safeRemaining};t=${retryAfter}`);
return retryAfter;
}
retryAfter is computed from reset (a millisecond timestamp) minus the current time, floored at zero so clock skew never produces a negative wait. X-RateLimit-Reset is sent as Unix epoch seconds, matching GitHub's convention. The IETF RateLimit-Policy value "anon";q=20;w=10 reads as "the anon policy allows a quota of 20 in a 10-second window," and RateLimit reports the live remaining quota and seconds left10.
Step 7 — Wire it together as Hono middleware
Now assemble the pieces into one reusable middleware. Hono's factory helper, createMiddleware, produces middleware with correct TypeScript types, including any context variables it sets11.
Create src/rate-limit-middleware.ts:
import { createMiddleware } from 'hono/factory';
import { getClientIp } from './client-ip.js';
import { limiters, type Tier } from './ratelimit.js';
import { resolveTier } from './tiers.js';
import { applyRateLimitHeaders } from './rate-limit-headers.js';
export type AppEnv = {
Variables: {
rateLimit: { tier: Tier; limit: number; remaining: number };
};
};
export function rateLimit() {
return createMiddleware<AppEnv>(async (c, next) => {
const apiKey = c.req.header('x-api-key');
const tier = resolveTier(apiKey);
const clientIp = getClientIp(c);
// Anonymous callers are limited per IP; authenticated callers per key.
const identifier = tier === 'anon' ? `ip:${clientIp}` : `key:${apiKey}`;
const result = await limiters[tier].limit(identifier, {
ip: clientIp,
userAgent: c.req.header('user-agent'),
});
// Flush analytics + deny-list updates in the background. A long-lived
// Node process stays alive, so there is no need to await this.
result.pending.catch(() => {});
const retryAfter = applyRateLimitHeaders(c, tier, result);
if (!result.success) {
if (result.reason === 'denyList') {
return c.json(
{ error: 'forbidden', message: 'This request was blocked.' },
403,
);
}
c.header('Retry-After', String(retryAfter));
return c.json(
{
error: 'too_many_requests',
message: `Rate limit exceeded. Retry in ${retryAfter}s.`,
retryAfter,
},
429,
);
}
c.set('rateLimit', {
tier,
limit: result.limit,
remaining: Math.max(0, result.remaining),
});
await next();
});
}
A few details worth calling out. The limit() call passes ip and userAgent alongside the identifier; Step 8 uses those for deny-list checks. The result.pending promise carries the analytics and deny-list writes — on a serverless platform you must hand it to waitUntil, but on a long-lived Node server the process stays alive long enough for it to finish, so a fire-and-forget .catch() is correct and keeps the response fast12. A deny-list hit returns 403 Forbidden rather than 429, because that caller is not "too fast" — they are blocked.
Update src/index.ts to apply the middleware and read its result:
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { env } from './env.js';
import { rateLimit, type AppEnv } from './rate-limit-middleware.js';
const app = new Hono<AppEnv>();
// Liveness probe — deliberately NOT rate limited.
app.get('/health', (c) => c.json({ status: 'ok' }));
// Every route under /api is rate limited.
app.use('/api/*', rateLimit());
app.get('/api/quote', (c) => {
const rl = c.get('rateLimit');
return c.json({
quote: 'Premature optimization is the root of all evil.',
tier: rl.tier,
remaining: rl.remaining,
});
});
const server = serve({ fetch: app.fetch, port: env.PORT }, (info) => {
console.log(`API listening on http://localhost:${info.port}`);
});
function shutdown(): void {
server.close(() => process.exit(0));
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
app.use('/api/*', rateLimit()) applies the limiter to every /api route while leaving /health untouched — a health probe that gets rate limited will eventually mark your service unhealthy under load. Because the app is typed with AppEnv, c.get('rateLimit') is fully typed inside the route.
Step 8 — Harden against floods and abuse
The limiter works, but under a real flood it has two weaknesses: every malicious request still costs Redis commands, and a Redis hiccup could take your API down with it. Three Ratelimit constructor options close those gaps and add a deny list for known-bad clients. Update buildLimiter in src/ratelimit.ts:
// Add near the top of src/ratelimit.ts, at module scope:
const ephemeralCache = new Map<string, number>();
function buildLimiter(tier: Tier): Ratelimit {
const cfg = TIERS[tier];
return new Ratelimit({
redis,
prefix: `rl:${tier}`,
limiter: Ratelimit.slidingWindow(cfg.limit, cfg.window),
analytics: true,
ephemeralCache, // block known-bad callers from memory, 0 Redis calls
timeout: 1000, // fail open if Redis is slow or unreachable
enableProtection: tier === 'anon', // deny-list checks for anon traffic
});
}
ephemeralCache is an in-process Map. Once a caller is rate limited, the limiter remembers their reset time in memory and rejects every further request from them without touching Redis at all, reporting reason: 'cacheBlock'13. During a flood from a handful of IPs, this collapses thousands of Redis commands to zero. Declare the Map at module scope so it survives across requests.
timeout: 1000 makes the limiter fail open. If the Redis call does not resolve within 1000 ms, the request is allowed through instead of rejected13. The default is 5000 ms. The reasoning: a rate limiter exists to protect your API, and it should never become the reason your API is down. A brief Redis outage degrades you to "unlimited" — annoying — rather than "offline."
enableProtection: true turns on deny lists14. With it on, the ip and userAgent values you already pass to limit() are checked against a deny list you manage in the Upstash Console's Rate Limit dashboard, plus an Auto IP Deny List built from open-source malicious-IP feeds and refreshed daily around 02:00 UTC. A match returns success: false with reason: 'denyList' — which the Step 7 middleware already turns into a 403. Deny-list checks add two Redis commands per call5, so this tutorial enables them only for the anonymous tier, where abuse concentrates.
To block a specific IP by hand, open the Rate Limit dashboard, select your database, and add the value to the deny list. Matching is exact — there is no wildcard or CIDR support — so add individual addresses.
Verification
Start the server with npm run dev. In a second terminal, confirm a single request succeeds and carries the headers:
curl -i http://localhost:3000/api/quote
You should see HTTP/1.1 200 OK plus X-RateLimit-Limit: 20, a decreasing X-RateLimit-Remaining, and the RateLimit / RateLimit-Policy fields.
Now exceed the anonymous limit of 20 requests per 10 seconds with a quick burst:
for i in $(seq 1 25); do
curl -s -o /dev/null -w "%{http_code} " http://localhost:3000/api/quote
done
echo
The first 20 requests print 200 and the rest print 429 — the sliding window in action. Inspect a rejected response and confirm it carries Retry-After:
curl -i http://localhost:3000/api/quote
# HTTP/1.1 429 Too Many Requests
# retry-after: 7
# {"error":"too_many_requests","message":"Rate limit exceeded. Retry in 7s.","retryAfter":7}
Finally, prove the tiers are independent. The paid demo key has a 1000-request limit, so it sails through while anonymous traffic is still blocked:
curl -s -H "x-api-key: demo-paid-key" http://localhost:3000/api/quote
# {"quote":"Premature optimization is the root of all evil.","tier":"paid","remaining":999}
With analytics: true, the allowed and blocked counts from these runs appear in the Upstash Console's Rate Limit dashboard.
Troubleshooting
Every request is rejected immediately, even the first. Your callers are probably all collapsing to one identifier. Log identifier at the top of the middleware: if it is the same string for different clients, getConnInfo is returning a single value (common when the app sits behind a proxy but TRUSTED_PROXY_HOPS is 0, so every request shows the proxy's IP). Set TRUSTED_PROXY_HOPS to your real proxy count.
Attackers are bypassing the limit entirely. You are almost certainly keying on the leftmost X-Forwarded-For value somewhere. That entry is client-supplied; an attacker rotates it every request and never shares a key. Use the trusted IP from Step 4 — socket address or rightmost trusted hop — everywhere a caller is identified.
Error: Missing required environment variable: UPSTASH_REDIS_REST_URL. The .env file is not being loaded. Confirm the file sits in the project root and the dev script includes --env-file=.env. Running tsx src/index.ts directly skips it, because tsx does not load .env on its own.
Requests succeed even though Redis credentials are wrong. That is the timeout option working as designed. With an unreachable Redis, limit() times out after 1000 ms and fails open, so the request is allowed with reason: 'timeout'. Fix the UPSTASH_REDIS_REST_URL and token; if you would rather fail closed, check result.reason === 'timeout' in the middleware and reject.
Analytics never appear in the dashboard. Two usual causes. The dashboard's prefix selector must match the prefix you set on the limiter (rl:anon, rl:free, rl:paid) — analytics are stored per prefix. And on a serverless platform the pending promise must be awaited or handed to waitUntil, or the process ends before the analytics write completes; on a long-lived Node server this is not an issue.
Next steps and further reading
You now have production-grade rate limiting: a sliding-window limiter, spoof-resistant caller identification, multi-tier limits, correct 429 responses, an ephemeral cache, a fail-open timeout, and deny lists. A few directions from here:
- Per-route limits. Apply a stricter limiter to expensive routes by creating another tier (for example a
searchlimiter at 5 requests per 10 seconds) and mounting it withapp.use('/api/search', searchRateLimit()). - Cost-weighted limits.
limiters.paid.limit(id, { rate: batchSize })subtracts more than one token for a heavier request — useful when a single call processes a batch13. - Cache the data behind the limiter. Pair rate limiting with a caching layer so the requests that do get through are cheap; see Redis caching patterns for scalable systems.
- Design the limits themselves. For how to choose fair, cost-aware limits across an API surface, see AI rate limiting: fairness, cost, and scale and API versioning strategies.
Footnotes
-
@upstash/ratelimit2.0.8 and@upstash/redis1.38.0, verified on the npm registry, 24 May 2026. https://www.npmjs.com/package/@upstash/ratelimit ↩ -
Node.js Releases — Node.js 24 is the current Active LTS line, supported through April 2028. https://nodejs.org/en/about/previous-releases ↩
-
Upstash Redis Pricing & Limits, accessed 24 May 2026. https://upstash.com/docs/redis/overall/pricing ↩
-
Ratelimiting Algorithms — fixed window, sliding window, and token bucket, Upstash Documentation. https://upstash.com/docs/redis/sdks/ratelimit-ts/algorithms ↩ ↩2 ↩3
-
Costs — Redis command counts per algorithm, analytics, and deny lists, Upstash Documentation. https://upstash.com/docs/redis/sdks/ratelimit-ts/costs ↩ ↩2
-
"The perils of the 'real' client IP," adam-p, on why security-sensitive code must use the rightmost trusted
X-Forwarded-Forentry. https://adam-p.ca/blog/2022/03/x-forwarded-for/ ↩ ↩2 -
ConnInfo Helper, Hono Documentation —
getConnInfofor the Node.js adapter. https://hono.dev/docs/helpers/conninfo ↩ -
RFC 6585, Additional HTTP Status Codes — Section 4, "429 Too Many Requests." https://www.rfc-editor.org/rfc/rfc6585 ↩
-
RFC 9110, HTTP Semantics — Section 10.2.3, the
Retry-Afterheader field. https://www.rfc-editor.org/rfc/rfc9110#section-10.2.3 ↩ -
"RateLimit header fields for HTTP," draft-ietf-httpapi-ratelimit-headers, IETF HTTPAPI Working Group, latest revision 18 April 2026 — still an Internet-Draft. https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/ ↩ ↩2
-
Factory Helper —
createMiddleware, Hono Documentation. https://hono.dev/docs/helpers/factory ↩ -
Methods — the
limit()response and thependingpromise, Upstash Documentation. https://upstash.com/docs/redis/sdks/ratelimit-ts/methods ↩ -
Features — caching, timeout, analytics, custom rates, Upstash Documentation. https://upstash.com/docs/redis/sdks/ratelimit-ts/features ↩ ↩2 ↩3
-
Traffic Protection — deny lists and the Auto IP Deny List, Upstash Documentation. https://upstash.com/docs/redis/sdks/ratelimit-ts/traffic-protection ↩