Node.jsでのcronジョブ——なぜ必要か
データ収集・レポート生成・期限チェックなど、「定期的に実行したい処理」はWebアプリに必ずと言っていいほど存在します。Node.jsでこれを実現する主な方法は以下の3つです。
- node-cron:npm packageで軽量に実装
- Agenda:MongoDBベースのジョブスケジューラー
- BullMQ:Redisベースの本格的なジョブキュー
本記事では最も手軽な node-cron を中心に解説し、実務で使えるパターンを紹介します。
node-cronのインストールと基本
npm install node-cron axios dotenv
const cron = require('node-cron');
// cron式:秒 分 時 日 月 曜日
// * * * * * *
// 毎分実行
cron.schedule('* * * * *', () => {
console.log('毎分実行:', new Date().toISOString());
});
// 毎朝9時に実行
cron.schedule('0 9 * * *', () => {
console.log('毎朝9時に実行');
}, {
timezone: 'Asia/Tokyo',
});
// 毎週月曜0時に実行
cron.schedule('0 0 * * 1', () => {
console.log('毎週月曜0時に実行');
}, { timezone: 'Asia/Tokyo' });
cron式の読み方
┌───────── 秒(0〜59)※省略可
│ ┌─────── 分(0〜59)
│ │ ┌───── 時(0〜23)
│ │ │ ┌─── 日(1〜31)
│ │ │ │ ┌─ 月(1〜12)
│ │ │ │ │ ┌ 曜日(0〜7、0と7が日曜)
│ │ │ │ │ │
* * * * * *
よく使うパターン:
"0 * * * *" → 毎時0分
"0 9 * * *" → 毎日9時
"0 9 * * 1-5" → 平日9時
"0 0 1 * *" → 毎月1日0時
"*/15 * * * *" → 15分ごと
"0 9,18 * * *" → 9時と18時
実践:ECサイトの価格監視ジョブ
require('dotenv').config();
const cron = require('node-cron');
const axios = require('axios');
const { JSDOM } = require('jsdom');
// 監視対象商品リスト
const watchList = [
{
name: 'ワイヤレスイヤホン A',
url: 'https://example-ec.com/products/1234',
selector: '.product-price',
targetPrice: 8000,
},
// ...
];
async function scrapePrices() {
const results = [];
for (const item of watchList) {
try {
const response = await axios.get(item.url, {
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; PriceBot/1.0)' },
timeout: 10000,
});
const dom = new JSDOM(response.data);
const priceText = dom.window.document.querySelector(item.selector)?.textContent ?? '';
const price = parseInt(priceText.replace(/[^\d]/g, ''), 10);
results.push({ ...item, currentPrice: price });
// 目標価格以下になったらSlack通知
if (price > 0 && price <= item.targetPrice) {
await notifySlack(item, price);
}
// 礼儀正しい待機
await sleep(1500 + Math.random() * 1000);
} catch (error) {
console.error(`Error fetching ${item.url}:`, error.message);
}
}
return results;
}
async function notifySlack(item, price) {
const payload = {
text: [
`:tada: *価格アラート*`,
`商品: ${item.name}`,
`現在価格: ¥${price.toLocaleString()}`,
`目標価格: ¥${item.targetPrice.toLocaleString()}`,
`URL: ${item.url}`,
].join('\n'),
};
await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 1時間ごとに価格チェック
cron.schedule('0 * * * *', async () => {
console.log(`[${new Date().toLocaleString('ja-JP')}] 価格チェック開始`);
const results = await scrapePrices();
console.log(`チェック完了: ${results.length}件`);
}, { timezone: 'Asia/Tokyo' });
console.log('価格監視ボット起動');
ジョブの排他制御——重複実行を防ぐ
ジョブの実行時間が長い場合、次の実行時刻までに終わらず重複実行が起きることがあります。
let isRunning = false;
cron.schedule('*/30 * * * *', async () => {
if (isRunning) {
console.log('前回のジョブがまだ実行中です。スキップします。');
return;
}
isRunning = true;
try {
await runHeavyJob();
} finally {
isRunning = false;
}
});
ジョブの結果をDBに記録する
const mysql = require('mysql2/promise');
async function saveJobResult(jobName, status, details) {
const conn = await mysql.createConnection(process.env.DATABASE_URL);
await conn.execute(
'INSERT INTO job_logs (job_name, status, details, executed_at) VALUES (?, ?, ?, NOW())',
[jobName, status, JSON.stringify(details)]
);
await conn.end();
}
// ジョブ内で使用
cron.schedule('0 3 * * *', async () => {
const startedAt = Date.now();
try {
const result = await runDataCollection();
await saveJobResult('data_collection', 'success', {
collected: result.length,
duration: Date.now() - startedAt,
});
} catch (error) {
await saveJobResult('data_collection', 'failure', {
error: error.message,
duration: Date.now() - startedAt,
});
}
});
PM2で本番運用
開発環境では node app.js でよいですが、本番環境ではPM2を使って常時起動します。
npm install -g pm2
# 起動
pm2 start app.js --name "price-bot"
# 自動起動設定
pm2 startup
pm2 save
# ログ確認
pm2 logs price-bot
# 再起動
pm2 restart price-bot
まとめ
node-cronを使えば、Node.jsアプリに定期実行処理を簡単に組み込めます。弊社ではEC価格監視・在庫チェック・データ収集など複数のcronジョブを本番稼働させています。PM2による常時起動と、Slack通知によるエラー監視を組み合わせることで、信頼性の高い自動化システムを実現しています。
業務自動化・データ収集システムの開発についてはお気軽にご相談ください。