Vercel 帳單暴增?5 個防護策略|2025 費用控制完整指南
一覺醒來,Vercel 帳單多了幾百美元。
這種噩夢確實發生過。
DDoS 攻擊、爬蟲、病毒式傳播...
各種原因都可能讓帳單失控。
這篇文章教你如何保護你的帳單。
Vercel 計費項目
費用組成
| 項目 | 免費額度 | 超出費用 |
|---|---|---|
| 頻寬 | 100 GB | $0.15/GB |
| Serverless 執行 | 100 GB-hrs | $0.18/GB-hr |
| Edge 請求 | 500,000 次 | $0.65/百萬次 |
| Edge 執行 | 1,000,000 GB-s | $0.018/GB-hr |
| Image Optimization | 1,000 張 | $5/1,000 張 |
| Build 分鐘 | 6,000 分鐘 | $0.01/分鐘 |
費用計算範例
假設你的網站突然爆紅:
一天 100 萬次訪問
- 頻寬:100 萬 × 500KB = 500 GB
- 超出:500 - 100 = 400 GB
- 費用:400 × $0.15 = $60/天
一個月:$60 × 30 = $1,800
加上 Serverless Functions、Edge Functions...
帳單可能更高。
策略一:設定 Spend Management
什麼是 Spend Management?
Vercel 提供的費用控制功能,可以設定預算上限。
Pro 和 Enterprise 方案可用。
設定步驟
- 進入 Vercel Dashboard
- 點擊 Settings → Billing
- 選擇 Spend Management
- 設定每月預算上限
設定選項
| 選項 | 說明 |
|---|---|
| Hard Limit | 達到上限後停止服務 |
| Soft Limit | 達到上限後發送通知 |
建議設定:
- Soft Limit:預算的 80%
- Hard Limit:預算的 100%
達到上限會怎樣?
Hard Limit 達到時:
- Serverless Functions 停止執行
- 靜態資源仍可訪問
- 新部署被阻止
恢復方法:
- 提高上限
- 等待下個計費週期
策略二:實作 API 限流
為什麼需要限流?
防止:
- 單一用戶過度使用
- API 被濫用
- 惡意攻擊
使用 Upstash Rate Limit
npm install @upstash/ratelimit @upstash/redis
// lib/ratelimit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 每 10 秒 10 次
analytics: true,
});
// app/api/protected/route.ts
import { ratelimit } from '@/lib/ratelimit';
import { headers } from 'next/headers';
export async function GET(request: Request) {
// 使用 IP 作為識別
const ip = headers().get('x-forwarded-for') ?? '127.0.0.1';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: '請求過於頻繁,請稍後再試' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
);
}
// 正常處理
return Response.json({ data: 'success' });
}
不同層級的限流
// 針對不同端點設定不同限制
const publicLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'), // 公開 API
});
const authLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(1000, '1 m'), // 已認證用戶
});
const aiLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'), // AI API(昂貴)
});
策略三:使用 Middleware 防護
全站防護
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'),
});
export async function middleware(request: NextRequest) {
// 只限制 API 路由
if (request.nextUrl.pathname.startsWith('/api')) {
const ip = request.ip ?? '127.0.0.1';
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
阻擋可疑 IP
// middleware.ts
const blockedIPs = new Set([
'1.2.3.4',
'5.6.7.8',
]);
const blockedCountries = new Set([
'XX', // 可疑國家
]);
export async function middleware(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1';
const country = request.geo?.country ?? '';
// 阻擋可疑 IP
if (blockedIPs.has(ip)) {
return new Response('Forbidden', { status: 403 });
}
// 阻擋可疑國家
if (blockedCountries.has(country)) {
return new Response('Forbidden', { status: 403 });
}
return NextResponse.next();
}
阻擋爬蟲
// middleware.ts
const blockedUserAgents = [
'bot',
'crawler',
'spider',
'scraper',
];
export async function middleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent')?.toLowerCase() ?? '';
// 阻擋可疑 User-Agent
if (blockedUserAgents.some(agent => userAgent.includes(agent))) {
// 允許好的爬蟲
if (userAgent.includes('googlebot') || userAgent.includes('bingbot')) {
return NextResponse.next();
}
return new Response('Forbidden', { status: 403 });
}
return NextResponse.next();
}
策略四:監控和告警
設定用量告警
在 Vercel Dashboard:
- Settings → Notifications
- 新增告警規則
- 設定觸發條件
告警類型:
- 用量達到 X%
- 費用達到 $X
- 異常流量檢測
自建監控
// lib/monitoring.ts
import { kv } from '@vercel/kv';
export async function trackUsage(event: string) {
const today = new Date().toISOString().split('T')[0];
const key = `usage:${event}:${today}`;
const count = await kv.incr(key);
// 檢查是否超過閾值
if (count > 10000) {
await sendAlert(`High usage detected: ${event} = ${count}`);
}
return count;
}
async function sendAlert(message: string) {
// 發送 Slack 通知
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
body: JSON.stringify({ text: `🚨 ${message}` }),
});
// 或發送郵件
// await sendEmail(process.env.ALERT_EMAIL!, message);
}
使用範例
// app/api/expensive/route.ts
import { trackUsage } from '@/lib/monitoring';
export async function POST(request: Request) {
// 追蹤使用量
await trackUsage('expensive-api');
// 處理請求
return Response.json({ success: true });
}
每日報告
// app/api/cron/daily-report/route.ts
export async function GET() {
const today = new Date().toISOString().split('T')[0];
const stats = {
apiCalls: await kv.get(`usage:api:${today}`) || 0,
aiCalls: await kv.get(`usage:ai:${today}`) || 0,
imageCalls: await kv.get(`usage:image:${today}`) || 0,
};
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
body: JSON.stringify({
text: `📊 Daily Report for ${today}\n` +
`API Calls: ${stats.apiCalls}\n` +
`AI Calls: ${stats.aiCalls}\n` +
`Image Calls: ${stats.imageCalls}`,
}),
});
return Response.json({ sent: true });
}
// vercel.json
{
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 9 * * *"
}
]
}
策略五:優化資源使用
減少頻寬使用
啟用壓縮:
// next.config.js
module.exports = {
compress: true,
};
使用 WebP 圖片:
import Image from 'next/image';
<Image
src="/photo.jpg"
width={800}
height={600}
quality={75} // 降低品質
format="webp"
/>
設定快取標頭:
// vercel.json
{
"headers": [
{
"source": "/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
減少 Serverless 執行時間
// 使用快取減少執行
import { unstable_cache } from 'next/cache';
const getCachedData = unstable_cache(
async () => {
return await expensiveQuery();
},
['data-key'],
{ revalidate: 300 }
);
export async function GET() {
const data = await getCachedData();
return Response.json(data);
}
減少 Image Optimization
// next.config.js
module.exports = {
images: {
// 限制優化的圖片來源
remotePatterns: [
{ hostname: 'trusted-cdn.com' },
],
// 或完全關閉
unoptimized: true,
},
};
使用 Edge Functions
Edge Functions 比 Serverless Functions 便宜:
| 項目 | Serverless | Edge |
|---|---|---|
| 計費單位 | GB-hr | 請求數 |
| 費用 | $0.18/GB-hr | $0.65/百萬次 |
對於簡單操作,Edge 更划算。
// app/api/fast/route.ts
export const runtime = 'edge';
export async function GET() {
return Response.json({ fast: true });
}
緊急應對:帳單已經暴增
立即行動
-
聯繫 Vercel 客服
- 說明情況
- 可能獲得減免 -
暫停服務
- 設定 Hard Limit
- 暫時下線 -
分析原因
- 查看 Analytics
- 檢查 Logs
常見原因和解決
| 原因 | 解決方法 |
|---|---|
| DDoS 攻擊 | 啟用 Cloudflare |
| 爬蟲 | 設定 robots.txt |
| 病毒式傳播 | 設定限流 |
| API 濫用 | 加強認證 |
| 程式錯誤 | 修復並重新部署 |
預防再次發生
- 設定 Spend Management
- 實作限流
- 設定監控告警
- 定期檢查用量
免費方案的安全使用
免費額度
| 項目 | 額度 |
|---|---|
| 頻寬 | 100 GB |
| Serverless | 100 GB-hrs |
| Build | 6,000 分鐘 |
如何不超額?
預估用量:
100 GB 頻寬 ÷ 500 KB/頁面 = 200,000 頁面訪問
如果每天 6,000 訪問:
6,000 × 30 = 180,000/月
應該夠用!
如果快超額:
- 優化資源大小
- 增加快取
- 減少 API 呼叫
- 考慮升級
帳單檢查清單
每週檢查
- [ ] 查看 Usage 頁面
- [ ] 確認沒有異常流量
- [ ] 檢查 Logs 有無錯誤
每月檢查
- [ ] 檢視帳單明細
- [ ] 分析各項目費用
- [ ] 優化高費用項目
- [ ] 更新限流設定
設定完成
- [ ] Spend Management 已設定
- [ ] 限流已實作
- [ ] 監控告警已啟用
- [ ] 緊急聯絡方式已準備
常見問題 FAQ
Q1:免費方案會有帳單嗎?
不會,Hobby 方案免費。
但如果升級到 Pro 且沒設定 Spend Management,可能會有意外費用。
Q2:設定 Hard Limit 後網站會掛嗎?
靜態資源仍可訪問,但 Serverless Functions 會停止。
建議設定 Soft Limit 先收到通知。
Q3:如何知道是否被攻擊?
- 流量突然暴增
- 來源 IP 集中
- 請求模式異常
Q4:Vercel 會退費嗎?
視情況而定。
如果是因為攻擊或平台問題,可以聯繫客服協商。
Q5:應該用 Cloudflare 嗎?
推薦使用 Cloudflare 作為額外防護:
- 免費 CDN
- DDoS 防護
- 可設定防火牆規則
Vercel 帳單防護五大策略重點整理
5 個防護策略:
- Spend Management - 設定預算上限
- API 限流 - 限制請求頻率
- Middleware 防護 - 阻擋惡意流量
- 監控告警 - 及時發現問題
- 優化資源 - 減少不必要費用
做好這些,帳單就在你的掌控中。
Vercel 部署失敗?
Build Error、環境變數、自訂網域,我們幫你快速排除問題。