Skip to content

VercelとRender.comの比較評価

分散システム・クラウドアーキテクチャの観点から、VercelRender.comの比較評価を行います。

⚠️ Vercelは、トランザクション内で外部APIを呼ぶ設計や、長時間実行が必要な非同期処理には不向きです。Outboxパターンなどの適切な設計により一部の問題は解決できますが、プロセス寿命の制約や再実行の不確実性により、Render.comなどの常駐環境が適切なケースがあります。

Render.comは、常駐プロセスとWorkerによる非同期処理が可能なため、トランザクション設計冪等性が重要なシステムに適しています。ただし、コストスケーラビリティのトレードオフを考慮する必要があります。

1. トランザクション内で外部APIを呼んでいるか?
→ YES: Render.comを検討(構造改善後もVercelは困難)
→ NO: 次のステップへ
2. 長時間実行(5分以上)の非同期処理が必要か?
→ YES: Render.comを検討
→ NO: 次のステップへ
3. プロセス状態を保持する必要があるか?
→ YES: Render.comを検討
→ NO: 次のステップへ
4. Outboxパターンを実装できるか?
→ YES: Vercelでも可能(ただし制約あり)
→ NO: Render.comを検討
5. コストを最優先するか?
→ YES: Vercelを検討(構造改善必須)
→ NO: Render.comを検討
6. スケーラビリティを最優先するか?
→ YES: Vercelを検討(構造改善必須)
→ NO: Render.comを検討

1. トランザクション内で外部APIを呼んではいけない理由

Section titled “1. トランザクション内で外部APIを呼んではいけない理由”

問題のあるコード:

// ❌ 悪い例: トランザクション内で外部APIを呼ぶ
async function createOrder(orderData: OrderData) {
return await db.transaction(async (tx) => {
// 1. 注文をデータベースに保存
const order = await tx.order.create({ data: orderData });
// 2. トランザクション内で外部APIを呼ぶ(問題)
const paymentResult = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId: order.id, amount: orderData.amount }),
});
if (!paymentResult.ok) {
throw new Error('Payment failed');
}
// 3. 決済結果を保存
await tx.payment.create({ data: { orderId: order.id, status: 'completed' } });
return order;
});
}

問題点:

  1. トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
  2. 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
  3. ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  4. タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする

影響:

  • データベースのパフォーマンス低下
  • デッドロックの発生
  • データの不整合
  • ユーザー体験の低下

改善された実装:

// ✅ 良い例: Outboxパターンを使用
async function createOrder(orderData: OrderData) {
return await db.transaction(async (tx) => {
// 1. 注文をデータベースに保存
const order = await tx.order.create({ data: orderData });
// 2. Outboxテーブルに外部API呼び出しのタスクを記録
await tx.outbox.create({
data: {
eventType: 'PAYMENT_CHARGE',
payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }),
status: 'PENDING',
idempotencyKey: `payment-${order.id}-${Date.now()}`,
},
});
// 3. トランザクションをコミット(外部APIは呼ばない)
return order;
});
}
// 別のプロセス/ワーカーでOutboxを処理
async function processOutbox() {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
try {
// 外部APIを呼ぶ
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: event.payload,
headers: {
'Idempotency-Key': event.idempotencyKey,
},
});
if (response.ok) {
// 成功したらOutboxを更新
await db.outbox.update({
where: { id: event.id },
data: { status: 'COMPLETED' },
});
} else {
// 失敗したらリトライ用に更新
await db.outbox.update({
where: { id: event.id },
data: {
status: 'FAILED',
retryCount: { increment: 1 },
},
});
}
} catch (error) {
// エラーハンドリング
await db.outbox.update({
where: { id: event.id },
data: {
status: 'FAILED',
retryCount: { increment: 1 },
},
});
}
}
}

メリット:

  • トランザクションの短縮: データベースのロック時間が短縮される
  • 外部障害の分離: 外部APIの障害がトランザクションに影響しない
  • 再実行の容易さ: Outboxテーブルから再実行可能
  • 冪等性の保証: 冪等キーにより重複実行を防止

2. Outboxパターンの役割と、なぜServerless環境と相性が良いか

Section titled “2. Outboxパターンの役割と、なぜServerless環境と相性が良いか”

定義: Outboxパターンは、トランザクション内で外部API呼び出しや非同期処理を記録し、別のプロセスで処理するパターンです。

役割:

  1. トランザクションの分離: データベーストランザクションと外部API呼び出しを分離
  2. 確実な配信: トランザクションがコミットされたら、確実に外部APIが呼ばれる
  3. 再実行の保証: 失敗した処理を再実行可能
  4. 冪等性の保証: 冪等キーにより重複実行を防止

なぜServerless環境と相性が良いか:

  1. イベント駆動: Serverlessはイベント駆動で動作するため、Outboxテーブルの変更をトリガーにできる
  2. スケーラビリティ: 需要に応じて自動的にスケールする
  3. コスト効率: 処理が必要な時だけ実行される
  4. 障害耐性: 個別の関数が失敗しても、他の処理に影響しない

実践例(Vercel):

// Outboxテーブルの定義
CREATE TABLE outbox (
id SERIAL PRIMARY KEY,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
retry_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
// Vercel CronでOutboxを処理
// vercel.json
{
"crons": [{
"path": "/api/process-outbox",
"schedule": "*/1 * * * *" // 1分ごと
}]
}
// api/process-outbox.ts
export default async function handler(req: Request) {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
await processEvent(event);
}
return Response.json({ processed: pendingEvents.length });
}

制約:

// VercelのServerless関数の制約
// - 実行時間: 最大10秒(Hobby)、60秒(Pro)、300秒(Enterprise)
// - メモリ: 最大1024MB
// - プロセス状態: 実行間で保持されない

問題のある実装:

// ❌ 悪い例: 長時間実行が必要な処理
export default async function handler(req: Request) {
// 10分かかる処理(Vercelでは不可能)
await longRunningProcess();
return Response.json({ success: true });
}

影響:

  • 長時間実行が必要な処理が実行できない
  • プロセス状態を保持できない
  • 接続プールなどの状態管理が困難

問題:

// VercelのServerless関数は、以下の場合に再実行される可能性がある
// 1. タイムアウト
// 2. メモリ不足
// 3. エラー発生
// 4. プラットフォームの都合
// 問題: 再実行が保証されない、または予期しない再実行が発生する

実践例:

// ❌ 悪い例: 冪等性がない処理
export default async function handler(req: Request) {
// 冪等キーがない場合、再実行で重複処理が発生する可能性
await processPayment(req.body.orderId);
return Response.json({ success: true });
}
// ✅ 良い例: 冪等キーを使用
export default async function handler(req: Request) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return Response.json({ error: 'Idempotency-Key required' }, { status: 400 });
}
// 冪等キーで重複実行を防止
const existing = await db.processedEvents.findUnique({
where: { idempotencyKey },
});
if (existing) {
return Response.json({ success: true, cached: true });
}
await processPayment(req.body.orderId);
await db.processedEvents.create({
data: { idempotencyKey, result: 'success' },
});
return Response.json({ success: true });
}

問題:

// VercelのServerless関数は、レスポンスを返すと終了する
// バックグラウンド処理は保証されない
// ❌ 悪い例: レスポンス後に処理を続行
export default async function handler(req: Request) {
// レスポンスを返す
const response = Response.json({ success: true });
// この処理は実行されない可能性がある
await sendEmail(req.body.email);
return response;
}

解決策:

// ✅ 良い例: Outboxパターンを使用
export default async function handler(req: Request) {
await db.transaction(async (tx) => {
await tx.order.create({ data: req.body });
// Outboxに記録
await tx.outbox.create({
data: {
eventType: 'SEND_EMAIL',
payload: JSON.stringify({ email: req.body.email }),
status: 'PENDING',
},
});
});
// レスポンスを返す(メール送信は別プロセスで処理)
return Response.json({ success: true });
}

特徴:

// Render.comのWebサービス
// - 常駐プロセス: 24時間実行される
// - プロセス状態: 保持される
// - 実行時間: 制限なし(実質的)
// - メモリ: プランに応じて設定可能

メリット:

  • 長時間実行が可能
  • プロセス状態を保持できる
  • 接続プールなどの状態管理が容易
  • バックグラウンド処理が可能

実践例:

server.ts
// Render.comのWebサービスでOutboxを処理
import express from 'express';
const app = express();
// Outboxを定期的に処理
setInterval(async () => {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
await processEvent(event);
}
}, 5000); // 5秒ごと
app.listen(3000);

特徴:

// Render.comのWorkerサービス
// - 専用のワーカープロセス
// - スケーラビリティ: 複数のワーカーを実行可能
// - キュー処理との相性が良い

実践例:

worker.ts
// Render.comのWorkerでOutboxを処理
import Bull from 'bull';
const outboxQueue = new Bull('outbox', {
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
// Outboxテーブルを監視してキューに追加
setInterval(async () => {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
await outboxQueue.add(event);
}
}, 5000);
// キューから処理
outboxQueue.process(async (job) => {
const event = job.data;
await processEvent(event);
});

実践例:

// Render.comのWorkerとBull/BullMQの組み合わせ
import Bull from 'bull';
const paymentQueue = new Bull('payment', {
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
// ジョブの処理
paymentQueue.process(async (job) => {
const { orderId, amount } = job.data;
// 外部APIを呼ぶ
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
headers: {
'Idempotency-Key': job.id, // ジョブIDを冪等キーとして使用
},
});
if (!response.ok) {
throw new Error('Payment failed');
}
return { success: true };
});
// リトライ設定
paymentQueue.on('failed', async (job, err) => {
if (job.attemptsMade < 3) {
// 3回までリトライ
await job.retry();
} else {
// 3回失敗したらアラート
await sendAlert(`Payment failed for order ${job.data.orderId}`);
}
});

5. 冪等キーがない場合に起こり得る事故

Section titled “5. 冪等キーがない場合に起こり得る事故”

シナリオ1: ネットワークエラーによる重複実行

// ❌ 悪い例: 冪等キーがない
async function chargePayment(orderId: string, amount: number) {
// 外部APIを呼ぶ
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
});
// ネットワークエラーが発生
// クライアントはタイムアウトエラーを受信
// しかし、サーバー側では処理が完了している可能性がある
// クライアントが再試行
// → 重複決済が発生
}

影響:

  • 重複決済
  • データの不整合
  • ユーザーの不満
  • 財務的な損失

解決策:

// ✅ 良い例: 冪等キーを使用
async function chargePayment(orderId: string, amount: number, idempotencyKey: string) {
// 冪等キーで重複実行を防止
const existing = await db.payments.findUnique({
where: { idempotencyKey },
});
if (existing) {
return existing; // 既に処理済み
}
// 外部APIを呼ぶ(冪等キーをヘッダーに含める)
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
headers: {
'Idempotency-Key': idempotencyKey,
},
});
// 結果を保存
const payment = await db.payments.create({
data: {
orderId,
amount,
idempotencyKey,
status: 'completed',
},
});
return payment;
}

シナリオ2: Serverless関数の再実行

// VercelのServerless関数が再実行される場合
// 1. タイムアウト
// 2. メモリ不足
// 3. プラットフォームの都合
// ❌ 悪い例: 冪等キーがない
export default async function handler(req: Request) {
// 処理が完了する前にタイムアウト
await longRunningProcess();
// 関数が再実行される
// → 重複処理が発生
}

解決策:

// ✅ 良い例: 冪等キーを使用
export default async function handler(req: Request) {
const idempotencyKey = req.headers['idempotency-key'] || generateIdempotencyKey();
// 冪等キーで重複実行を防止
const existing = await db.processedEvents.findUnique({
where: { idempotencyKey },
});
if (existing) {
return Response.json(existing.result);
}
const result = await longRunningProcess();
// 結果を保存
await db.processedEvents.create({
data: {
idempotencyKey,
result: JSON.stringify(result),
},
});
return Response.json(result);
}

6. 構造を直せばVercelで問題ないケース

Section titled “6. 構造を直せばVercelで問題ないケース”

ケース1: Outboxパターンを実装できる場合

Section titled “ケース1: Outboxパターンを実装できる場合”

条件:

  • Outboxパターンを実装できる
  • 処理時間が10秒以内(Hobby)、60秒以内(Pro)、300秒以内(Enterprise)
  • 冪等キーを実装できる
  • 非同期処理がVercel Cronで処理可能

実践例:

// ✅ Vercelで問題ないケース
// 1. Outboxパターンを実装
async function createOrder(orderData: OrderData) {
return await db.transaction(async (tx) => {
const order = await tx.order.create({ data: orderData });
// Outboxに記録
await tx.outbox.create({
data: {
eventType: 'PAYMENT_CHARGE',
payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }),
status: 'PENDING',
idempotencyKey: `payment-${order.id}-${Date.now()}`,
},
});
return order;
});
}
// 2. Vercel CronでOutboxを処理
// vercel.json
{
"crons": [{
"path": "/api/process-outbox",
"schedule": "*/1 * * * *"
}]
}
// api/process-outbox.ts
export default async function handler(req: Request) {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
// 10秒以内で処理可能
await processEvent(event);
}
return Response.json({ processed: pendingEvents.length });
}

条件:

  • 外部API呼び出しが同期処理のみ
  • 処理時間が短い(10秒以内)
  • 冪等キーを実装できる

実践例:

// ✅ Vercelで問題ないケース
export default async function handler(req: Request) {
const idempotencyKey = req.headers['idempotency-key'];
// 冪等キーで重複実行を防止
const existing = await db.processedEvents.findUnique({
where: { idempotencyKey },
});
if (existing) {
return Response.json(existing.result);
}
// 短時間の同期処理
const result = await shortSyncProcess();
await db.processedEvents.create({
data: {
idempotencyKey,
result: JSON.stringify(result),
},
});
return Response.json(result);
}

7. 構造を直してもVercelでは厳しいケース

Section titled “7. 構造を直してもVercelでは厳しいケース”

ケース1: 長時間実行が必要な処理

Section titled “ケース1: 長時間実行が必要な処理”

条件:

  • 処理時間が10分以上
  • プロセス状態を保持する必要がある
  • ストリーミング処理が必要

実践例:

// ❌ Vercelでは困難なケース
// 1. 長時間実行が必要な処理
async function processLargeFile(fileId: string) {
// 10分かかる処理
await processFile(fileId);
// Vercelのタイムアウト(最大300秒)を超える
}
// 2. ストリーミング処理
async function streamData() {
// ストリーミング処理はVercelでは困難
const stream = createReadStream('large-file.csv');
// 処理...
}

解決策: Render.comを使用

// ✅ Render.comで解決
// Render.comのWebサービスで長時間実行
app.post('/api/process-large-file', async (req, res) => {
const { fileId } = req.body;
// バックグラウンドで処理
processLargeFile(fileId).then(() => {
// 完了通知
});
res.json({ status: 'processing' });
});

ケース2: リアルタイム処理が必要な場合

Section titled “ケース2: リアルタイム処理が必要な場合”

条件:

  • WebSocket接続が必要
  • 長時間の接続を保持する必要がある
  • プロセス状態を保持する必要がある

実践例:

// ❌ Vercelでは困難なケース
// WebSocket接続はVercelでは困難
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
// 長時間の接続を保持
// VercelのServerless関数では困難
});

解決策: Render.comを使用

// ✅ Render.comで解決
// Render.comのWebサービスでWebSocket
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
// 長時間の接続を保持可能
ws.on('message', (message) => {
// メッセージ処理
});
});

ケース3: 複雑なキュー処理が必要な場合

Section titled “ケース3: 複雑なキュー処理が必要な場合”

条件:

  • 優先度付きキュー
  • 遅延実行
  • 複雑なリトライロジック

実践例:

// ❌ Vercelでは困難なケース
// 複雑なキュー処理はVercel Cronでは困難
// - 優先度付きキュー
// - 遅延実行
// - 複雑なリトライロジック

解決策: Render.comを使用

// ✅ Render.comで解決
// Render.comのWorkerでBull/BullMQを使用
import Bull from 'bull';
const queue = new Bull('tasks', {
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
// 優先度付きキュー
queue.add({ task: 'high-priority' }, { priority: 1 });
queue.add({ task: 'low-priority' }, { priority: 10 });
// 遅延実行
queue.add({ task: 'delayed' }, { delay: 60000 }); // 1分後
// 複雑なリトライロジック
queue.process(async (job) => {
try {
await processTask(job.data);
} catch (error) {
if (job.attemptsMade < 3) {
throw error; // リトライ
} else {
// 3回失敗したら別のキューに移動
await failedQueue.add(job.data);
}
}
});

8. Render.com(または常駐環境)を選ぶべき判断基準

Section titled “8. Render.com(または常駐環境)を選ぶべき判断基準”

1. 処理時間が長い(5分以上)

// 判断基準: 処理時間が5分以上
// → Render.comを選ぶ
// 例: 大容量ファイルの処理
async function processLargeFile(fileId: string) {
// 10分かかる処理
await processFile(fileId);
}

2. プロセス状態を保持する必要がある

// 判断基準: プロセス状態を保持する必要がある
// → Render.comを選ぶ
// 例: 接続プール、セッション管理
const connectionPool = new ConnectionPool();
// プロセス間で状態を保持
app.use((req, res, next) => {
req.db = connectionPool.getConnection();
next();
});

3. リアルタイム処理が必要

// 判断基準: WebSocket、Server-Sent Eventsが必要
// → Render.comを選ぶ
// 例: リアルタイムチャット
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
// 長時間の接続を保持
});

4. 複雑なキュー処理が必要

// 判断基準: 優先度付きキュー、遅延実行、複雑なリトライロジック
// → Render.comを選ぶ
// 例: Bull/BullMQを使用したキュー処理
const queue = new Bull('tasks');
queue.add({ task: 'high-priority' }, { priority: 1 });

5. トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)

// 判断基準: 構造改善が困難で、トランザクション内で外部APIを呼ぶ必要がある
// → Render.comを選ぶ
// 例: レガシーコードの移行が困難
async function legacyFunction() {
return await db.transaction(async (tx) => {
// 構造改善が困難な場合
const result = await externalAPI.call();
// ...
});
}

9. 段階的な改善ステップ(全移行しない現実解)

Section titled “9. 段階的な改善ステップ(全移行しない現実解)”

ステップ1: 現状分析と優先度付け

Section titled “ステップ1: 現状分析と優先度付け”

実施内容:

// 1. 現状のコードを分析
// - トランザクション内で外部APIを呼んでいる箇所を特定
// - 処理時間が長い箇所を特定
// - 冪等キーがない箇所を特定
// 2. 優先度を決定
const priorities = [
{ issue: 'トランザクション内で外部APIを呼ぶ', priority: 'HIGH' },
{ issue: '冪等キーがない', priority: 'HIGH' },
{ issue: '処理時間が長い', priority: 'MEDIUM' },
{ issue: 'エラーハンドリングが不十分', priority: 'MEDIUM' },
];

ステップ2: 緊急度の高い箇所から改善

Section titled “ステップ2: 緊急度の高い箇所から改善”

実施内容:

// 1. トランザクション内で外部APIを呼んでいる箇所をOutboxパターンに変更
// 例: 決済処理
async function createOrder(orderData: OrderData) {
return await db.transaction(async (tx) => {
const order = await tx.order.create({ data: orderData });
// Outboxに記録
await tx.outbox.create({
data: {
eventType: 'PAYMENT_CHARGE',
payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }),
status: 'PENDING',
idempotencyKey: `payment-${order.id}-${Date.now()}`,
},
});
return order;
});
}
// 2. 冪等キーを追加
// すべての外部API呼び出しに冪等キーを追加
async function callExternalAPI(data: any, idempotencyKey: string) {
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Idempotency-Key': idempotencyKey,
},
});
return response.json();
}

ステップ3: Vercel CronでOutboxを処理

Section titled “ステップ3: Vercel CronでOutboxを処理”

実施内容:

// 1. Vercel Cronを設定
// vercel.json
{
"crons": [{
"path": "/api/process-outbox",
"schedule": "*/1 * * * *" // 1分ごと
}]
}
// 2. Outbox処理関数を実装
// api/process-outbox.ts
export default async function handler(req: Request) {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
try {
await processEvent(event);
} catch (error) {
// エラーハンドリング
await db.outbox.update({
where: { id: event.id },
data: {
status: 'FAILED',
retryCount: { increment: 1 },
},
});
}
}
return Response.json({ processed: pendingEvents.length });
}

ステップ4: 困難な箇所をRender.comに移行

Section titled “ステップ4: 困難な箇所をRender.comに移行”

実施内容:

// 1. 長時間実行が必要な処理をRender.comに移行
// Render.comのWebサービスで処理
app.post('/api/process-large-file', async (req, res) => {
const { fileId } = req.body;
// バックグラウンドで処理
processLargeFile(fileId).then(() => {
// 完了通知
});
res.json({ status: 'processing' });
});
// 2. VercelからRender.comのAPIを呼ぶ
// api/process-file.ts
export default async function handler(req: Request) {
const { fileId } = await req.json();
// Render.comのAPIを呼ぶ
const response = await fetch('https://your-app.onrender.com/api/process-large-file', {
method: 'POST',
body: JSON.stringify({ fileId }),
});
return Response.json(await response.json());
}

実施内容:

Next.js
// 1. Vercel: フロントエンド、API、短時間処理
// - API: 短時間の同期処理
// - Outbox処理: Vercel Cron
// 2. Render.com: 長時間処理、キュー処理
// - 長時間実行が必要な処理
// - キュー処理(Bull/BullMQ)
// - WebSocket接続
// 3. 連携方法
// - VercelからRender.comのAPIを呼ぶ
// - Render.comからVercelのAPIを呼ぶ
// - データベースを共有

条件:

  • 処理時間が短い(10秒以内、Hobby)
  • 同期処理のみ
  • Outboxパターンを実装できる
  • 冪等キーを実装できる
  • スケーラビリティが重要
  • コストを抑えたい

実践例:

// ✅ Vercel向き
// 1. 短時間の同期処理
export default async function handler(req: Request) {
const result = await shortSyncProcess();
return Response.json(result);
}
// 2. Outboxパターンを実装した非同期処理
export default async function handler(req: Request) {
await db.transaction(async (tx) => {
await tx.order.create({ data: req.body });
await tx.outbox.create({ data: { eventType: 'SEND_EMAIL', ... } });
});
return Response.json({ success: true });
}

条件:

  • 処理時間が長い(5分以上)
  • プロセス状態を保持する必要がある
  • リアルタイム処理が必要
  • 複雑なキュー処理が必要
  • トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)

実践例:

// ❌ Vercel不向き
// 1. 長時間実行が必要な処理
export default async function handler(req: Request) {
// 10分かかる処理(Vercelでは不可能)
await longRunningProcess();
return Response.json({ success: true });
}
// 2. WebSocket接続
const wss = new WebSocketServer({ port: 8080 });
// Vercelでは困難

条件:

  • 処理時間が長い(5分以上)
  • プロセス状態を保持する必要がある
  • リアルタイム処理が必要
  • 複雑なキュー処理が必要
  • トランザクション内で外部APIを呼ぶ必要がある(構造改善が困難)

実践例:

// ✅ Render.com向き
// 1. 長時間実行が必要な処理
app.post('/api/process-large-file', async (req, res) => {
const { fileId } = req.body;
await processLargeFile(fileId); // 10分かかる処理
res.json({ success: true });
});
// 2. WebSocket接続
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
// 長時間の接続を保持
});

条件:

  • スケーラビリティが最重要
  • コストを最小限に抑えたい
  • 短時間の同期処理のみ
  • トラフィックが不安定

実践例:

// ❌ Render.com不向き
// 1. 短時間の同期処理のみ(Vercelの方が適している)
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});

構成:

┌─────────────────┐
│ Vercel │
│ - Frontend │
│ - API Routes │
│ - Cron Jobs │
└────────┬────────┘
│ HTTP API
┌────────▼────────┐
│ Render.com │
│ - Web Service │
│ - Workers │
│ - Queue │
└────────┬────────┘
┌────────▼────────┐
│ Database │
│ - PostgreSQL │
│ - Redis │
└─────────────────┘

実装例:

app/api/orders/route.ts
// Vercel: フロントエンドとAPI
export async function POST(req: Request) {
const orderData = await req.json();
// Outboxパターンで記録
const order = await db.transaction(async (tx) => {
const order = await tx.order.create({ data: orderData });
await tx.outbox.create({
data: {
eventType: 'PROCESS_ORDER',
payload: JSON.stringify({ orderId: order.id }),
status: 'PENDING',
idempotencyKey: `order-${order.id}`,
},
});
return order;
});
return Response.json(order);
}
// Vercel Cron: Outbox処理
// api/process-outbox.ts
export default async function handler(req: Request) {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
take: 10,
});
for (const event of pendingEvents) {
// Render.comのAPIを呼ぶ(長時間処理が必要な場合)
if (event.eventType === 'PROCESS_LARGE_FILE') {
await fetch('https://your-app.onrender.com/api/process', {
method: 'POST',
body: event.payload,
});
} else {
// 短時間処理はVercelで処理
await processEvent(event);
}
}
return Response.json({ processed: pendingEvents.length });
}
// Render.com: 長時間処理とキュー
// server.ts
import express from 'express';
import Bull from 'bull';
const app = express();
const queue = new Bull('tasks', {
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
// 長時間処理
app.post('/api/process', async (req, res) => {
const { orderId } = req.body;
// キューに追加
await queue.add({ orderId }, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
res.json({ status: 'queued' });
});
// キュー処理
queue.process(async (job) => {
const { orderId } = job.data;
// 長時間処理
await processOrder(orderId);
});

今すぐやるべき対応(優先度順)

Section titled “今すぐやるべき対応(優先度順)”

優先度1: トランザクション内の外部API呼び出しを排除

Section titled “優先度1: トランザクション内の外部API呼び出しを排除”

実施内容:

// 1. トランザクション内で外部APIを呼んでいる箇所を特定
// 2. Outboxパターンに変更
// 3. 冪等キーを追加
// 例: 決済処理
async function createOrder(orderData: OrderData) {
return await db.transaction(async (tx) => {
const order = await tx.order.create({ data: orderData });
// Outboxに記録
await tx.outbox.create({
data: {
eventType: 'PAYMENT_CHARGE',
payload: JSON.stringify({ orderId: order.id, amount: orderData.amount }),
status: 'PENDING',
idempotencyKey: `payment-${order.id}-${Date.now()}`,
},
});
return order;
});
}

実施内容:

// 1. すべての外部API呼び出しに冪等キーを追加
// 2. データベースに冪等キーのインデックスを作成
// 3. 重複実行を防止
// 例: 冪等キーの実装
async function callExternalAPI(data: any, idempotencyKey: string) {
// 既に処理済みか確認
const existing = await db.processedEvents.findUnique({
where: { idempotencyKey },
});
if (existing) {
return existing.result;
}
// 外部APIを呼ぶ
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Idempotency-Key': idempotencyKey,
},
});
const result = await response.json();
// 結果を保存
await db.processedEvents.create({
data: {
idempotencyKey,
result: JSON.stringify(result),
},
});
return result;
}

実施内容:

// 1. Outboxテーブルを作成
CREATE TABLE outbox (
id SERIAL PRIMARY KEY,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
retry_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_outbox_status ON outbox(status);
CREATE INDEX idx_outbox_idempotency ON outbox(idempotency_key);
// 2. Vercel CronでOutboxを処理
// vercel.json
{
"crons": [{
"path": "/api/process-outbox",
"schedule": "*/1 * * * *"
}]
}
// 3. Outbox処理関数を実装
// api/process-outbox.ts
export default async function handler(req: Request) {
const pendingEvents = await db.outbox.findMany({
where: { status: 'PENDING' },
orderBy: { createdAt: 'asc' },
take: 10,
});
for (const event of pendingEvents) {
try {
await processEvent(event);
await db.outbox.update({
where: { id: event.id },
data: { status: 'COMPLETED' },
});
} catch (error) {
await db.outbox.update({
where: { id: event.id },
data: {
status: 'FAILED',
retryCount: { increment: 1 },
},
});
}
}
return Response.json({ processed: pendingEvents.length });
}

実施内容:

// 1. 長時間処理が必要な箇所を特定
// 2. Render.comに移行するか、非同期処理に変更
// 例: 大容量ファイルの処理
// VercelからRender.comのAPIを呼ぶ
export default async function handler(req: Request) {
const { fileId } = await req.json();
// Render.comのAPIを呼ぶ
const response = await fetch('https://your-app.onrender.com/api/process-large-file', {
method: 'POST',
body: JSON.stringify({ fileId }),
});
return Response.json(await response.json());
}

優先度5: エラーハンドリングとリトライの実装

Section titled “優先度5: エラーハンドリングとリトライの実装”

実施内容:

// 1. エラーハンドリングを実装
// 2. リトライロジックを実装
// 3. アラートを設定
// 例: リトライロジック
async function processEvent(event: OutboxEvent) {
const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) {
try {
await callExternalAPI(JSON.parse(event.payload), event.idempotencyKey);
return;
} catch (error) {
if (i === maxRetries - 1) {
// 最大リトライ回数に達したらアラート
await sendAlert(`Failed to process event ${event.id} after ${maxRetries} retries`);
throw error;
}
// 指数バックオフでリトライ
await sleep(Math.pow(2, i) * 1000);
}
}
}

VercelとRender.comの比較評価のポイント:

  • 結論: Vercelは適切な設計により一部の問題は解決できるが、プロセス寿命の制約によりRender.comが適切なケースがある
  • 判断フローチャート: トランザクション、処理時間、プロセス状態、Outboxパターン、コスト、スケーラビリティを考慮
  • Vercel向き: 短時間処理、Outboxパターン実装可能、スケーラビリティ重視
  • Render.com向き: 長時間処理、プロセス状態保持、リアルタイム処理、複雑なキュー処理
  • 推奨アーキテクチャ: ハイブリッド構成(Vercel + Render.com)
  • 今すぐやるべき対応: トランザクション内の外部API呼び出しを排除、冪等キーの実装、Outbox処理の実装、長時間処理の分離、エラーハンドリングとリトライの実装

適切な設計と実行環境の選択により、効率的で安全なシステムを構築できます。