Skip to content

Commit 0091e33

Browse files
authored
Merge pull request #65 from replicate/rate-limit
Add rate limiting for API endpoints
2 parents db2c57b + 3c00ccb commit 0091e33

File tree

4 files changed

+149
-4
lines changed

4 files changed

+149
-4
lines changed

middleware.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { Ratelimit } from '@upstash/ratelimit';
3+
import { kv } from '@vercel/kv';
4+
5+
const ratelimit = new Ratelimit({
6+
redis: kv,
7+
// 20 requests from the same IP within a 10 second sliding window
8+
limiter: Ratelimit.slidingWindow(20, '10s'),
9+
prefix: `v2/zoo/ratelimit/${process.env.VERCEL_ENV ?? 'local'}`,
10+
timeout: 500,
11+
});
12+
13+
// Rate limit the /api/predictions/[id] endpoint
14+
export const config = {
15+
matcher: ['/api/predictions/:path+'],
16+
};
17+
18+
export default async function middleware(request: NextRequest) {
19+
if (!process.env.VERCEL_ENV || !process.env.KV_REST_API_URL || !process.env.KV_REST_API_URL) {
20+
console.warn('Skipping ratelimiting middleware');
21+
return NextResponse.next();
22+
}
23+
24+
const ip = request.ip ?? '127.0.0.1';
25+
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
26+
const headers = {
27+
'X-Ratelimit-Hit': String(!success),
28+
'X-Ratelimit-Limit': String(limit),
29+
'X-Ratelimit-Remaining': String(remaining),
30+
'X-Ratelimit-Reset': String(reset),
31+
}
32+
33+
return success
34+
? NextResponse.next({headers})
35+
: NextResponse.json({}, { status: 429, headers });
36+
}

package-lock.json

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"@heroicons/react": "^2.0.18",
1515
"@prisma/client": "^4.14.0",
1616
"@supabase/supabase-js": "^2.24.0",
17+
"@upstash/ratelimit": "^1.0.0",
1718
"@vercel/analytics": "^1.0.1",
19+
"@vercel/kv": "^1.0.1",
1820
"@vercel/og": "^0.5.4",
1921
"@vercel/postgres": "^0.3.0",
2022
"eslint": "8.29.0",

pages/index.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export default function Home({ baseUrl, submissionPredictions }) {
3636
const response = await fetch(`/api/submissions/${seed}`, {
3737
method: "GET",
3838
});
39-
submissionPredictions = await response.json();
39+
if (response.ok) {
40+
submissionPredictions = await response.json();
41+
}
4042
setPredictions(submissionPredictions);
4143

4244
// get the model names from the predictions, and update which ones are checked
@@ -151,17 +153,36 @@ export default function Home({ baseUrl, submissionPredictions }) {
151153
throw new Error(prediction.detail);
152154
}
153155

156+
// Add incremental backoff for polling requests.
157+
const backoff = [250, 500, 500, 750, 1000, 1500, 3000, 5000, 10000, 15000, 30000];
158+
154159
while (
155160
prediction.status !== "succeeded" &&
156161
prediction.status !== "failed"
157162
) {
158-
await sleep(500);
163+
const jitter = random(0, 100); // Don't make all requests at the same time.
164+
const delay = backoff.shift();
165+
if (!delay) {
166+
// We've exceeded our timeout.
167+
// TODO: Better user facing messaging here.
168+
break;
169+
}
170+
171+
await sleep(delay + jitter);
159172
const response = await fetch("/api/predictions/" + prediction.id);
160-
prediction = await response.json();
161-
console.log(prediction);
173+
174+
// Handle Rate Limiting
175+
if (response.status === 429) {
176+
const reset = response.headers.get('X-Ratelimit-Reset') ?? Date.now() + 10_000;
177+
const wait = reset - Date.now();
178+
await sleep(wait)
179+
continue;
180+
}
181+
162182
if (response.status !== 200) {
163183
throw new Error(prediction.detail);
164184
}
185+
prediction = await response.json();
165186
}
166187

167188
prediction.model = model.name;
@@ -551,3 +572,7 @@ export async function getServerSideProps({ req }) {
551572

552573
return { props: { baseUrl, submissionPredictions } };
553574
}
575+
576+
function random(min, max) {
577+
return Math.floor(Math.random() * (max - min + 1) + min)
578+
}

0 commit comments

Comments
 (0)