Node.jsで定期的なバックグラウンド処理を実行する際、古くから親しまれているのがsetInterval関数です。

現代のアプリケーション開発において、一定の間隔で特定のタスクを繰り返す仕組みは、ログの定期出力やヘルスチェックなど多岐にわたる場面で利用されています。

しかし、Node.jsのイベントループの仕組みを正しく理解せずに使用すると、予期せぬパフォーマンスの低下やメモリリークを引き起こすリスクがあります。

本記事では、2026年現在のNode.jsにおけるsetIntervalの最適な活用方法と、最新の制御手法について詳しく掘り下げていきます。

setIntervalの基本的な動作と仕組み

Node.jsにおけるsetIntervalは、指定したミリ秒ごとにコールバック関数を実行し続けるためのタイマー機能です。

この関数は、Node.jsのグローバルオブジェクトとして定義されているため、モジュールのインポートなしで使用することが可能です。

基本的な構文は以下の通りであり、第1引数に実行したい関数、第2引数に実行間隔を指定します。

JavaScript
// 基本的なsetIntervalの使用例
const intervalId = setInterval(() => {
    console.log("3秒ごとにこのメッセージが表示されます");
}, 3000);

// 10秒後にタイマーを停止する例
setTimeout(() => {
    clearInterval(intervalId);
    console.log("タイマーを停止しました");
}, 10000);
実行結果
3秒ごとにこのメッセージが表示されます
3秒ごとにこのメッセージが表示されます
3秒ごとにこのメッセージが表示されます
タイマーを停止しました

Node.jsのタイマーは、イベントループの「Timerフェーズ」で管理されています。

OSのタイマー機能に依存しているため、指定した時間が経過した直後に必ず実行されることが保証されているわけではない点に注意が必要です。

イベントループが他の重い処理を実行している場合、タイマーのコールバック関数の実行はその処理が完了するまで遅延します。

setIntervalが抱える実行タイミングの問題点

setIntervalを使用する上で最も注意すべき点は、「前回の処理が終わっているかどうかに関わらず、次の実行がスケジュールされる」という性質です。

例えば、実行間隔を1秒に設定した処理の内容が、実際には2秒かかってしまう場合を想定してください。

このとき、最初の処理が終わる前に2回目の実行タイミングが到来し、イベントループのキューに次のタスクが積み重なっていきます。

これが繰り返されると、処理の滞留が発生し、アプリケーション全体のレスポンスが悪化する原因となります。

このような「実行の重なり」を回避するためには、再帰的なsetTimeoutの使用を検討するのが一般的です。

再帰的なsetTimeoutであれば、現在の処理が完了してから次の実行時間を設定するため、処理が重なるリスクを排除できます。

JavaScript
// 実行の重なりを防ぐ再帰的なsetTimeoutのパターン
const task = () => {
    console.log("非同期処理を開始します...");
    
    // 非同期処理をシミュレート
    setTimeout(() => {
        console.log("非同期処理が完了しました");
        // 次の実行をスケジュールする
        setTimeout(task, 1000);
    }, 2000);
};

task();

メモリリークを引き起こす主な原因と対策

Node.jsアプリケーションが長時間稼働し続けるサーバーサイドでは、メモリリークは致命的な問題となります。

setIntervalに関連するメモリリークの多くは、不要になったタイマーをクリアし忘れることに起因します。

タイマーが動いている間、そのコールバック関数が参照している変数やクロージャ内のオブジェクトはガベージコレクションの対象になりません。

特に、ReactやVueなどのフロントエンドフレームワークの思想に近いコンポーネント単位の設計をNode.jsで行っている場合、オブジェクトが破棄されてもタイマーだけが生き残るケースが散見されます。

参照の保持によるメモリ蓄積

以下の表は、setIntervalがメモリリークを引き起こす主な要因をまとめたものです。

原因詳細対策
clearIntervalの失念特定の条件で処理を停止させるロジックがないfinallyブロックや早期リターンで確実に停止させる
巨大なオブジェクトの参照コールバック内で外部スコープの大きな配列などを保持し続ける必要なデータのみを引数として渡し、不要な参照を断つ
未処理の例外エラー発生時にタイマーを止める処理が入っていないtry-catchを実装し、エラー時にタイマーを破棄する

unref()メソッドの活用

Node.js特有の機能として、タイマーオブジェクトに対してunref()メソッドを呼び出す手法があります。

通常、アクティブなタイマーが存在する場合、Node.jsのプロセスは終了しません。

しかし、unref()を呼び出すと、「そのタイマー以外にイベントループで待機している処理がない場合」に、タイマーが残っていてもプロセスを終了させることができます。

これにより、バックグラウンドの監視処理などが原因でプロセスが終了しなくなるトラブルを防ぐことが可能です。

JavaScript
const timer = setInterval(() => {
    console.log("この処理はプロセスを維持しません");
}, 1000);

// プロセスの終了を妨げないように設定
timer.unref();

AbortSignalによる現代的なタイマー制御

Node.js v15以降、AbortControllerおよびAbortSignalというWeb標準のAPIが広く普及しました。

2026年現在では、従来のclearIntervalに代わる、より洗練されたタイマー制御手法として定着しています。

AbortSignalを使用すると、複数の非同期処理やタイマーを一括でキャンセルできるという大きなメリットがあります。

timers/promisesモジュールの利用

Node.jsには標準でtimers/promisesモジュールが用意されており、これを利用することでsetIntervalを非同期反復処理(async iterator)として扱うことができます。

この手法では、AbortSignalをオプションとして渡すことで、外部からエレガントにループを停止させることが可能です。

JavaScript
const { setInterval } = require('node:timers/promises');

async function runTask() {
    const controller = new AbortController();
    const { signal } = controller;

    // 5秒後にタスクを中断させる
    setTimeout(() => controller.abort(), 5000);

    try {
        console.log("定期実行を開始します。");
        // setIntervalを非同期反復として利用
        for await (const startTime of setInterval(1000, Date.now(), { signal })) {
            console.log(`実行中... 開始時刻: ${new Date(startTime).toLocaleTimeString()}`);
        }
    } catch (err) {
        if (err.name === 'AbortError') {
            console.log("AbortSignalにより処理が中断されました。");
        } else {
            throw err;
        }
    }
}

runTask();
実行結果
定期実行を開始します。
実行中... 開始時刻: 12:00:01
実行中... 開始時刻: 12:00:02
実行中... 開始時刻: 12:00:03
実行中... 開始時刻: 12:00:04
AbortSignalにより処理が中断されました。

このコードの優れた点は、「タイマーの停止」という副作用を命令的に記述するのではなく、「中断信号」という宣言的な方法で管理できることにあります。

複雑なアプリケーションにおいて、キャンセルロジックを1か所に集約できるため、コードの可読性と保守性が大幅に向上します。

高精度なタイミングが求められる場合の注意点

setIntervalは高い精度が求められる音楽アプリケーションやリアルタイム制御には不向きです。

前述の通り、Node.jsのイベントループのビジー状態により、ミリ秒単位のズレが容易に発生するためです。

より正確な時間間隔が必要な場合は、高解像度タイマーであるprocess.hrtime.bigint()を使用して経過時間を手動で計算するか、Worker Threadsを使用してメインスレッドのブロックを回避する必要があります。

特に、CPU集約型の処理が同じスレッドで動いている場合、タイマーの間隔は設定値よりも大幅に長くなる可能性があります。

このようなケースでは、定期実行するタスク自体を別のスレッドに分離し、メッセージングを介して結果を受け取る構成が推奨されます。

Worker Threads内でのsetInterval利用

Worker Threadsを使用すると、メインスレッドの負荷に影響を受けにくい独立したイベントループでタイマーを動かすことができます。

これにより、メインスレッドで重いHTTPリクエスト処理やデータ加工が行われていても、定期的なタスクの遅延を最小限に抑えることが可能です。

スレッド間通信のコストは発生しますが、2026年のマルチコアCPUが一般的な環境では、信頼性を担保するための極めて有効な手段となります。

まとめ

Node.jsのsetIntervalは、非常にシンプルで使い勝手の良い関数ですが、その裏側にあるイベントループの性質を理解しておくことが欠かせません。

実行間隔が処理時間を下回ることで発生するタスクの滞留や、clearIntervalの忘れによるメモリリークは、運用後のトラブルを引き起こす定番の原因です。

2026年現在の開発においては、従来のID管理による停止方法だけでなく、AbortControllerを用いたスマートなキャンセル処理の実装が標準的なスキルとなっています。

また、プロセスの終了を考慮したunref()の使い分けや、高負荷時のWorker Threadsへの移行など、状況に応じた適切な手法を選択することが重要です。

本記事で紹介したテクニックを組み合わせることで、堅牢でメモリ効率の高いNode.jsアプリケーションを構築することができるでしょう。