Vercel에서 서버리스 함수 실행 시간 제한
Vercel 서버리스 함수의 실행 시간 제한을 이해하고, 이를 효과적으로 관리하는 방법을 알아봅니다.
Vercel에서 서버리스 함수 실행 시간 제한
Introduction
Vercel 서버리스 함수는 강력하지만, 실행 시간 제한이 있습니다. 이 제한을 이해하지 못하면 예상치 못한 에러나 성능 문제를 경험할 수 있습니다. 이 글에서는 Vercel 서버리스 함수의 실행 시간 제한을 분석하고, 이를 효과적으로 관리하는 방법을 알아보겠습니다.
Environment
- Vercel 프로젝트 (Next.js 14+)
- Node.js 18+
- Vercel Pro 플랜
- PostgreSQL 데이터베이스
Problem
서버리스 함수가 간헐적으로 타임아웃 에러를 발생시킵니다:
Error: FUNCTION_INVOCATION_TIMEOUT
Function execution timed out.
Duration: 30000ms
Limit: 10000ms특히 데이터베이스 쿼리가 복잡하거나 외부 API를 호출할 때 문제가 자주 발생합니다.
Analysis
Vercel 서버리스 함수의 실행 시간 제한은 플랜에 따라 다릅니다:
- Hobby: 10초 (기본)
- Pro: 60초 (최대)
- Enterprise: 900초 (최대)
실행 시간을 초과하면 함수가 강제로 종료되고 FUNCTION_INVOCATION_TIMEOUT 에러가 발생합니다. 이는 다음 원인으로 인해 발생할 수 있습니다:
- 데이터베이스 쿼리 비효율성
- 외부 API 호출 지연
- 불필요한 동기 처리
- 메모리 부족으로 인한 GC 지연
Solution
1. 함수 실행 시간 설정
vercel.json에서 함수별로 실행 시간을 설정할 수 있습니다:
{
"functions": {
"app/api/heavy-query/**/*.js": {
"maxDuration": 60
},
"app/api/quick-endpoint/**/*.js": {
"maxDuration": 10
}
}
}2. 데이터베이스 쿼리 최적화
// Before: 비효율적인 쿼리
async function getUserData(userId) {
const user = await prisma.user.findUnique({
where: { id: userId }
});
const posts = await prisma.post.findMany({
where: { authorId: userId }
});
const comments = await prisma.comment.findMany({
where: { authorId: userId }
});
return { user, posts, comments };
}
// After: 최적화된 쿼리
async function getUserData(userId) {
return await prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
select: {
id: true,
title: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 10
},
comments: {
select: {
id: true,
content: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 10
}
}
});
}3. 외부 API 호출 최적화
// Promise.all을 사용한 병렬 처리
async function fetchExternalData() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders()
]);
return { users, products, orders };
}
// 타임아웃 설정
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
}
}4. 캐싱 전략 구현
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN
});
async function getCachedData(key, fetchFn, ttl = 3600) {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fetchFn();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// 사용 예시
async function getUserProfile(userId) {
return getCachedData(
`user:${userId}`,
() => prisma.user.findUnique({ where: { id: userId } }),
1800
);
}5. 청크 단위 처리
// 대량 데이터 처리 시 청크 단위로 나누어 처리
async function processLargeDataset() {
const chunkSize = 100;
let offset = 0;
let hasMore = true;
while (hasMore) {
const chunk = await prisma.data.findMany({
skip: offset,
take: chunkSize,
orderBy: { id: 'asc' }
});
await processChunk(chunk);
offset += chunkSize;
hasMore = chunk.length === chunkSize;
// 청크 처리 후 잠시 대기
await new Promise(resolve => setTimeout(resolve, 100));
}
}6. 모니터링 설정
// 실행 시간 추적
function withTiming(fn) {
return async function(...args) {
const start = Date.now();
try {
const result = await fn(...args);
const duration = Date.now() - start;
console.log(`Function executed in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`Function failed after ${duration}ms:`, error.message);
throw error;
}
};
}Lessons Learned
- 플랜 선택의 중요성: 프로젝트의 요구사항에 맞는 Vercel 플랜을 선택해야 합니다.
- 데이터베이스 쿼리 최적화: 인덱스 활용과 쿼리 최적화는 필수입니다.
- 캐싱 전략: 자주 사용되는 데이터는 캐싱하여 함수 실행 시간을 줄여야 합니다.
- 병렬 처리: 독립적인 작업은 병렬로 처리하여 전체 실행 시간을 줄일 수 있습니다.
- 모니터링: 함수 실행 시간을 지속적으로 모니터링하여 문제를 조기에 발견해야 합니다.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.