Vercelデプロイエラーの大半は6つに分類できる。環境変数の欠落、TypeScript/ESLintのビルドエラー、Functionタイムアウト、メモリ上限、本番限定404、キャッシュ不整合だ。この記事では全パターンの具体的な解決策を解説する。
32blog.comをVercelに本番デプロイするとき、ローカルでは npm run build が通るのにVercelに上げた瞬間にビルドが死ぬ——そういう経験を何度もしてきた。「ローカルでは動く」は「Vercelでも動く」を意味しない。環境変数、Node.jsバージョン、キャッシュの扱い、Function制限……ローカルとVercelの差異は思ったより大きい。
7つのエラーパターンと解決策を網羅しているので、エラーメッセージが出たらCtrl+Fで検索してほしい。
ビルドがローカルでは通るのにVercelで失敗する
最も頻繁に遭遇するパターンだ。原因のほとんどは「環境変数の未設定」か「依存関係の解決失敗」のどちらかに絞られる。
環境変数が設定されていない
ローカルでは .env.local に書いた値が自動で読まれる。Vercelでは環境変数をダッシュボードから明示的に設定しないと読まれない。
ビルド中に環境変数が必要なコードが実行されると、こんなエラーになる。
Error: Missing required environment variable: DATABASE_URL
Build failed with errors
または、環境変数が undefined のまま処理が進んでクラッシュするパターンもある。
TypeError: Cannot read properties of undefined (reading 'split')
解決策: Vercelダッシュボード → Project → Settings → Environment Variables に追加する。
# ローカルの .env.local を確認
cat .env.local
# 以下のような変数がある場合はすべてVercelに追加が必要
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
AUTH_SECRET=...
環境変数を設定した後は「Redeploy」ではなく、新しいコミットをプッシュするか「Redeploy without cache」を選ぶと確実にビルドが通る。
TypeScriptの型エラー
ローカルでは npm run dev が --turbopack で起動していて型チェックをスキップしているが、Vercelのビルドは next build を実行するため型エラーで止まる。
Type error: Property 'name' does not exist on type 'User | null'
解決策: デプロイ前にローカルで必ず型チェックを走らせる習慣をつける。
# ビルド前に必ず実行する
npx tsc --noEmit
# 型エラーを修正してからプッシュ
next.config.ts で型チェックを無効化する方法もあるが、本番コードの品質が下がるので非推奨だ。
// next.config.ts(非推奨: 型チェックを無効化する設定)
const nextConfig = {
typescript: {
ignoreBuildErrors: true, // やむを得ない場合のみ
},
};
ESLintエラー
next build はESLintも実行する。npm run dev では引っかからなかったESLintエラーがビルド時に出ることがある。
./app/components/Button.tsx
ESLint: 'onClick' is defined but never used. (no-unused-vars)
Failed to compile.
解決策: ローカルで事前にLintを通す。
# ビルド前にLintを確認
npm run lint
# 自動修正できるものは修正
npm run lint -- --fix
急ぎの場合は next.config.ts でLintを一時的に無効化できるが、技術的負債になるので必ず後で修正する。
// next.config.ts
const nextConfig = {
eslint: {
ignoreDuringBuilds: true, // 一時対応のみ
},
};
Function Timeoutエラーが出る
Vercel Functionsにはタイムアウト上限がある。Fluid Compute(2025年4月以降のデフォルト)で制限値が大幅に緩和された。
| プラン | デフォルト | 最大 |
|---|---|---|
| Hobby | 300秒(5分) | 300秒(5分) |
| Pro | 300秒(5分) | 800秒(13分) |
| Enterprise | 300秒(5分) | 800秒(13分) |
Error: Task timed out after 300.00 seconds
原因と確認方法
VercelダッシュボードのFunctionsタブを開くと、各関数の実行時間が確認できる。300秒あっても上限に迫っているものがあれば要対応だ。
よくある原因は以下の通り。
- 外部APIへのリクエストが遅い
- データベースのクエリが最適化されていない
- 大きなファイルをメモリ上で処理している
- 無限ループや意図しない再帰
解決策1: maxDurationを設定する
// app/api/heavy-task/route.ts
export const maxDuration = 60; // Hobby最大300秒、Pro最大800秒
export async function GET() {
// 重い処理
const result = await heavyDatabaseQuery();
return Response.json(result);
}
解決策2: 処理を非同期キューに逃がす
重い処理はAPIルートで同期的に実行せず、バックグラウンドジョブに委ねる。Vercelなら Vercel Cron Jobs や外部サービス(Upstash QStash など)を使う方法がある。
// app/api/trigger-job/route.ts
import { Client } from "@upstash/qstash";
const qstash = new Client({ token: process.env.QSTASH_TOKEN! });
export async function POST(request: Request) {
const body = await request.json();
// 重い処理を非同期キューに投げる
await qstash.publishJSON({
url: `${process.env.VERCEL_URL}/api/process-job`,
body: body,
});
// すぐにレスポンスを返す
return Response.json({ status: "queued" });
}
解決策3: Node.jsランタイムを使う(Edgeはレガシー)
Vercelは現在、ほとんどのケースでNode.jsランタイムを推奨している。Fluid Computeにより、Node.js Functionsのコールドスタートが高速化し、メモリ上限(最大4 GB)やタイムアウト(最大800秒)もEdgeより大きい。Edge Runtimeは初回レスポンス25秒制限・コードサイズ1〜2 MB制限があり、レガシー扱いだ。
// app/api/fast-api/route.ts
// Node.jsランタイムがデフォルト — 設定不要
// Edge位置での実行が必要なときだけEdgeを使う
export async function GET() {
return Response.json({ status: "ok" });
}
メモリ不足(OOM)エラー
ビルド中または実行中にメモリが足りなくなると、このエラーが出る。
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
または Vercel のログに以下のように記録される。
Error: Function exceeded memory limit
Fluid Computeでのメモリ上限: Hobbyは2 GB / 1 vCPU、Pro/Enterpriseは最大4 GB / 2 vCPU。
ビルド時のメモリ不足
大規模なプロジェクトや、ビルド時に多くのページを静的生成する場合に起きやすい。
# Node.jsのメモリ制限を増やしてビルド
NODE_OPTIONS="--max-old-space-size=4096" npm run build
Vercelのビルド設定でも同様に環境変数を追加できる。
# Vercelダッシュボード > Environment Variables
NODE_OPTIONS = --max-old-space-size=4096
実行時のメモリ不足
APIルートで大きなデータをメモリ上に展開している場合に発生する。
// ❌ 悪い例: 大きなJSONを一度にメモリに展開
export async function GET() {
const hugeData = await fetchAllRecords(); // 100MB のデータ
return Response.json(hugeData);
}
// ✅ 良い例: ストリーミングレスポンスを使う
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const records = await fetchRecordsInBatches();
for await (const batch of records) {
controller.enqueue(
new TextEncoder().encode(JSON.stringify(batch) + "\n")
);
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "application/json" },
});
}
404エラーが本番環境だけで出る
ローカルでは正常なのに、Vercelにデプロイすると特定のページが404になるパターンがある。
動的ルートの静的生成漏れ(generateStaticParams)
// app/blog/[slug]/page.tsx
// generateStaticParamsが不完全だと本番でのみ404になる
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
generateStaticParams が返すスラッグ以外のパスにアクセスすると404になる。dynamicParams を true(デフォルト)にすると、未生成のパスにもアクセスできるようになる。
// app/blog/[slug]/page.tsx
// dynamicParams のデフォルトは true なので、未生成パスもオンデマンドで生成される
export const dynamicParams = true;
// dynamicParams = false にすると生成されていないパスは明示的に404になる
next-intl のルーティング設定ミス
多言語対応している場合、next-intlのロケール設定が本番環境と開発環境でズレることがある。セットアップの詳細はnext-intl多言語対応ガイドを参照。
// middleware.ts
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
locales: ["ja", "en", "es"],
defaultLocale: "ja",
// localePrefix: "always" の場合、/ja/... が必須になる
localePrefix: "as-needed", // デフォルトロケールはプレフィックスなし
});
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
キャッシュが古い状態で固まる
Vercelのキャッシュ戦略が予期しない動作を引き起こすことがある。特に fetch のキャッシュとISR(Incremental Static Regeneration)の組み合わせで起きやすい。App Routerのキャッシュの仕組みについてはSSRガイドも参考にしてほしい。
// ❌ キャッシュ戦略を明示していない
export async function getArticles() {
const res = await fetch("https://api.example.com/articles");
// Next.js 15+ではデフォルトno-store。明示しないと意図が不明確
return res.json();
}
// ✅ 適切なキャッシュ戦略を明示する
export async function getArticles() {
const res = await fetch("https://api.example.com/articles", {
// 60秒ごとに再検証
next: { revalidate: 60 },
});
return res.json();
}
// または完全にキャッシュを無効化
export async function getLatestPrice() {
const res = await fetch("https://api.example.com/price", {
cache: "no-store",
});
return res.json();
}
On-Demand Revalidationを使う
コンテンツ更新があった際に即座にキャッシュを破棄したい場合は、Revalidation APIを使う。
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token");
// 不正なリクエストを拒否
if (token !== process.env.REVALIDATION_TOKEN) {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
const body = await request.json();
if (body.path) {
revalidatePath(body.path);
}
if (body.tag) {
revalidateTag(body.tag);
}
return Response.json({ revalidated: true });
}
デプロイは成功するがCORSエラーが出る
フロントエンドからAPIを叩いたとき、本番環境だけCORSエラーが出るケースがある。
Access to fetch at 'https://api.example.com' from origin 'https://32blog.com'
has been blocked by CORS policy
Next.jsのAPIルートにCORSヘッダーを追加することで解決できる。
// app/api/data/route.ts
export async function GET(request: Request) {
const response = await fetchData();
return Response.json(response, {
headers: {
"Access-Control-Allow-Origin": "https://32blog.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
// OPTIONSリクエスト(preflight)にも応答する
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "https://32blog.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
または next.config.ts でまとめて設定する。
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "https://32blog.com" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST,OPTIONS" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type" },
],
},
];
},
};
export default nextConfig;
Access-Control-Allow-Origin: * はすべてのオリジンからのアクセスを許可する。パブリックなAPIなら問題ないが、認証が必要なAPIでは特定のオリジンのみ許可するよう制限すること。
FAQ
ローカルでビルド通るのにVercelで失敗するのはなぜ?
最も多い原因は環境変数の欠落だ。.env.local はローカル開発で自動的に読まれるが、Vercelではダッシュボードから明示的に設定する必要がある。次に多いのはTypeScript型エラー — Turbopackを使う npm run dev は型チェックをスキップするが、Vercelの next build はしない。push前に npx tsc --noEmit を走らせよう。
VercelのFunctionタイムアウト上限は今いくつ?
Fluid Compute(2025年4月以降のデフォルト)で全プランのデフォルトが300秒になった。Pro/Enterpriseは maxDuration で最大800秒まで延長可能。Fluid Computeに移行していないレガシープロジェクトは旧制限(Hobby: 10秒、Pro: 300秒)が適用される。
Edge RuntimeとNode.jsランタイム、どっちを使うべき?
Vercelは現在Node.jsを推奨している。Fluid ComputeによりNode.js Functionsのコールドスタートが高速化し、メモリ上限(最大4 GB)もタイムアウト(最大800秒)もEdgeより大きい。Edge Runtimeは初回レスポンス25秒制限・コードサイズ1〜2 MB制限があり、レガシー扱いだ。Edge位置での実行が必要なときだけ使おう。
「Function exceeded memory limit」エラーの対処法は?
Fluid Computeでのデフォルトメモリは2 GB(Pro/Enterpriseは最大4 GB)。ビルド時のOOMなら NODE_OPTIONS=--max-old-space-size=4096 を環境変数に設定する。実行時のOOMなら、大きなデータを一括でメモリに展開するのをやめてストリーミングレスポンスに切り替える。
本番環境だけ404が出るのはなぜ?
generateStaticParamsが不完全か、ミドルウェアの設定ミスが原因なことが多い。dynamicParams が false の場合、generateStaticParams が返さないパスは404になる。next-intlのmiddleware matcherが _next、api、静的ファイルを正しく除外しているかも確認しよう。
Vercelのキャッシュを強制クリアするには?
VercelダッシュボードからRedeploy without cacheを選ぶか、Route Handlerでオンデマンド再検証(revalidatePath() / revalidateTag())をトリガーする。ISRページなら revalidate の間隔を明示的に設定すればキャッシュが自動更新される。
Vercel Fluid Computeで何が変わった?
Fluid Compute(2025年4月以降のデフォルト)で従来のAWS Lambdaベースから移行した。主な変更: タイムアウトのデフォルトが10〜15秒→300秒、メモリが1 GB→2 GB(Pro最大4 GB)、課金がActive CPU時間ベース(I/O待ちのアイドル時間はカウントされない)、Edge Runtimeがレガシー扱いに。
まとめ
Vercelデプロイで踏むエラーのパターンをまとめる。
| エラー | 原因 | 解決策 |
|---|---|---|
| ビルド失敗 | 環境変数未設定、型エラー、ESLintエラー | Vercelに環境変数を追加。tsc --noEmit で事前確認 |
| Function Timeout | 処理が遅い、無限ループ | maxDuration(Pro最大800秒)、非同期キュー |
| OOMエラー | メモリ不足(デフォルト2 GB) | NODE_OPTIONS=--max-old-space-size=4096、ストリーミング |
| 404(本番のみ) | 動的ルートの生成漏れ、ミドルウェアのmatcher設定ミス | dynamicParams、matcherを見直す |
| キャッシュが古い | fetch のキャッシュ設定不備 | revalidate または cache: "no-store" を明示 |
| CORSエラー | CORSヘッダー未設定 | APIルートまたは next.config.ts でヘッダーを追加 |
「ローカルで動く = Vercelで動く」ではない。デプロイ前に tsc --noEmit と npm run lint を走らせるのを習慣にするだけで、ビルド起因のエラーの半分は事前に潰せる。残りは環境変数の確認と、VercelのFunctionsログを丁寧に読むことで解決できる。
関連記事: