Hey, the website I made was showcased on a twitch stream, which caused a sudden traffic spike of approximately 200-400 users. I’m on a hobby plan, and I treat this project as a learning playground to figure out all things vercel. The logs show errors seen in the image below, I can’t find any information on the topic and would love to find out more, as I’d love to prevent that in the future - if possible.
Some things that came to my mind are that the page on initial load performs 6 fetches (100 users each) to the twitch.tv API to grab viewer count info. Perhaps it’s hitting a rate limit, but I can’t know for sure - couldn’t reproduce it locally.
If I understood and implemented caching correctly, it shouldn’t be an issue as I want to keep the response in cache for 5 minutes, but perhaps I’m not understanding something very simple here - I’m a newbie to the more ‘backend’ parts of the framework.
I’ll attach the /streamers route contents below:
import db from '@/lib/db';
import { streamers } from '@/lib/schema';
import { NextResponse } from 'next/server';
import { getTwitchToken } from '../utils';
async function validateToken(token: string) {
const response = await fetch('https://id.twitch.tv/oauth2/validate', {
headers: {
Authorization: `OAuth ${token}`,
},
});
if (!response.ok) {
console.error('Token validation failed:', await response.text());
return false;
}
return true;
}
async function fetchStreamerBatch(usernames: string[], initialToken: string) {
let token = initialToken;
const isValid = await validateToken(token);
if (!isValid) {
console.log('Token invalid, attempting to get new token...');
token = await getTwitchToken();
if (!token) {
throw new Error(
'Failed to get new Twitch token after validation failure'
);
}
}
const queryString = usernames.map((name) => `user_login=${name}`).join('&');
console.log(
`Attempting Twitch API call with token: ${token.substring(0, 10)}...`
);
const response = await fetch(
`https://api.twitch.tv/helix/streams?${queryString}`,
{
headers: {
'Client-ID': process.env.TWITCH_CLIENT_ID!,
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Twitch API error: ${response.status} - ${errorText}. Client ID: ${process.env.TWITCH_CLIENT_ID?.substring(0, 5)}...`
);
}
return (await response.json()).data;
}
export async function GET(request: Request) {
try {
const [allStreamers, token] = await Promise.all([
db
.select({
id: streamers.id,
name: streamers.name,
twitchUrl: streamers.twitchUrl,
isSweat: streamers.isSweat,
avatarUrl: streamers.avatarUrl,
})
.from(streamers),
getTwitchToken(),
]);
if (!token) {
console.error('Token retrieval failed');
throw new Error(
'Failed to get Twitch token - token is null or undefined'
);
}
const usernames = allStreamers
.map((streamer) => streamer.twitchUrl?.split('/').pop())
.filter((username): username is string => !!username);
const OPTIMIZED_BATCH_SIZE = 100;
const batchPromises = Array.from(
{ length: Math.ceil(usernames.length / OPTIMIZED_BATCH_SIZE) },
(_, i) => {
const start = i * OPTIMIZED_BATCH_SIZE;
const batchUsernames = usernames.slice(
start,
start + OPTIMIZED_BATCH_SIZE
);
return fetchStreamerBatch(batchUsernames, token);
}
);
const streamDataArrays = await Promise.all(batchPromises);
const streamData = streamDataArrays.flat();
const streamDataMap = new Map(
streamData.map((stream) => [stream.user_login.toLowerCase(), stream])
);
const liveStreamers = allStreamers
.map((streamer) => {
if (!streamer.twitchUrl) {
return null;
}
const twitchUsername = streamer.twitchUrl
.split('/')
.pop()
?.toLowerCase();
if (!twitchUsername) {
return null;
}
const twitchData = streamDataMap.get(twitchUsername);
if (!twitchData) {
return null;
}
return {
id: streamer.id,
name: streamer.name,
twitchUrl: streamer.twitchUrl,
isSweat: streamer.isSweat,
avatarUrl: streamer.avatarUrl,
viewerCount: twitchData.viewer_count,
live: twitchData.type === 'live',
gameName: twitchData.game_name,
};
})
.filter(
(streamer): streamer is NonNullable<typeof streamer> =>
streamer !== null
);
return NextResponse.json(liveStreamers, {
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60',
},
});
} catch (error) {
console.error('Detailed error in streamer fetch:', error);
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: 'Failed to fetch streamer status',
},
{ status: 500 }
);
}
}
Thanks for any help in advance!