Node.jsにおけるアプリケーション開発では、特定の時間だけ処理を一時停止させる「スリープ」機能が必要になる場面が多々あります。

例えば、外部APIへのリクエスト制限を回避するためのレートリミット対策や、再試行処理(リトライ)の合間の待機時間の設定などが挙げられます。

しかし、Node.jsには他の言語で見られるような同期的な sleep() 関数が標準では組み込まれていません。

これは、Node.jsがシングルスレッドで動作するイベントループモデルを採用しており、スレッドを完全に止めてしまうことがアプリケーション全体の停止を意味するためです。

2026年現在のモダンな開発環境では、Promiseや async/await を活用した非同期的なスリープ実装が標準となっています。

本記事では、Node.jsで安全かつ効率的にスリープ処理を実装するための最新手法について、基礎から応用まで詳しく解説します。

なぜNode.jsには標準のsleep関数が存在しないのか

Node.jsの最大の特徴は、「ノンブロッキングI/O」という仕組みに基づいた設計にあります。

もしC言語やPythonのような同期的な sleep() をメインスレッドで実行すると、その間すべてのユーザーリクエストやタイマー処理が完全に止まってしまいます。

このようなブロッキング処理は、Webサーバーのレスポンス性能を著しく低下させるため、Node.jsの設計思想とは相反するものです。

そのため、Node.jsでは古くから setTimeout というコールバック形式の関数を用いて、指定時間後に処理を再開させる手法が取られてきました。

しかし、コールバック関数による実装は、処理が複雑になると「コールバック地獄」と呼ばれる可読性の低いコードを生む原因となります。

近年のNode.jsでは、これらの問題を解決するために、Promiseベースの非同期待機が推奨されています。

イベントループを止めずに「実行権を一度手放し、指定時間後に戻ってくる」という考え方が、Node.jsにおけるスリープの正体です。

従来のsetTimeoutによる待機処理

最も基本的な待機方法は、ブラウザのJavaScriptでもおなじみの setTimeout 関数を使用することです。

この方法は、指定したミリ秒が経過した後に、第1引数に渡した関数を実行するというものです。

JavaScript
// 従来のコールバック方式
console.log("処理を開始します");

setTimeout(() => {
  console.log("2秒経過しました");
}, 2000);

console.log("この行はすぐに実行されます");
実行結果
処理を開始します
この行はすぐに実行されます
2秒経過しました

実行結果から分かる通り、setTimeout 自体は非同期関数であるため、後続の処理が先に実行されてしまいます。

特定の処理を順番に実行したい場合、すべてのコードを setTimeout の中に記述しなければならず、コードの階層が深くなる欠点があります。

この課題を解決するために登場したのが、次に解説するPromiseを用いた実装です。

Promiseを活用したカスタムsleep関数の実装

モダンなJavaScriptでは、Promise オブジェクトと async/await 構文を組み合わせることで、同期的なコードに近い見た目でスリープを記述できます。

以下は、Promiseを自作してスリープを実現する一般的な手法です。

JavaScript
// Promiseを利用したsleep関数の定義
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function main() {
  console.log("1秒待機します...");
  
  // awaitを使って待機
  await sleep(1000);
  
  console.log("待機が完了しました");
}

main();

この実装のポイントは、Promiseの状態がresolve(解決)されるまで await が処理を一時停止させる点にあります。

await sleep(1000) と記述することで、メインスレッドをブロックすることなく、その関数内の実行順序のみを制御できます。

この手法は汎用性が高く、Node.jsのバージョンを問わず広く利用されてきました。

最新の推奨手法:node:timers/promisesモジュールの活用

現在のNode.jsにおいて最も推奨されるスリープの実装方法は、標準ライブラリの node:timers/promises を使用することです。

Node.js v15以降で導入されたこのモジュールを使用すれば、自分でPromiseをラップした関数を作成する必要がありません。

標準ライブラリとして提供されているため、信頼性が高く、コードの簡略化にもつながります。

基本的な使い方

node:timers/promises から setTimeout をインポートして使用します。

これは従来のグローバルな setTimeout とは異なり、最初からPromiseを返すように設計されています。

JavaScript
// ESM(ECMAScript Modules)での記述
import { setTimeout } from 'node:timers/promises';

async function execute() {
  console.log("3秒間のスリープを開始します");

  // 直接awaitで待機可能
  await setTimeout(3000);

  console.log("3秒経過しました");
}

execute();

モジュールをインポートするだけで、非常に直感的で美しいコードが書けるようになります。

2026年のプロジェクトにおいては、自作のsleep関数を作成するよりも、この標準モジュールを使用するのがベストプラクティスと言えます。

AbortSignalによるスリープの解除

node:timers/promisessetTimeout が優れている点は、「途中でスリープをキャンセルできる」という点にあります。

これは標準の AbortController APIと組み合わせることで実現可能です。

JavaScript
import { setTimeout } from 'node:timers/promises';

const ac = new AbortController();
const signal = ac.signal;

async function task() {
  try {
    console.log("長時間スリープ(10秒)を開始します...");
    
    // signalを渡すことで、外部からキャンセル可能になる
    await setTimeout(10000, undefined, { signal });
    
    console.log("スリープが完了しました");
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log("スリープが途中でキャンセルされました!");
    } else {
      throw err;
    }
  }
}

task();

// 2秒後にキャンセルを実行
global.setTimeout(() => {
  ac.abort();
}, 2000);
実行結果
長時間スリープ(10秒)を開始します...
スリープが途中でキャンセルされました!

この仕組みを利用すれば、ユーザーの操作やタイムアウト監視によって、不要になった待機処理を即座に終了させることができます。

これは、大規模なアプリケーションにおいてリソースを効率的に管理するために非常に重要なテクニックです。

各実装手法の比較

Node.jsで利用可能なスリープ手法の特徴を以下の表にまとめました。

手法推奨度特徴主な用途
setTimeout (Callback)非推奨ではないがコードが煩雑になる単純な遅延実行
Promise + setTimeout環境を選ばず動作する。定番の手法共通ライブラリの作成など
node:timers/promises標準機能で最も洗練されている。キャンセル可能モダンなNode.js開発全般
Atomics.wait (WorkerThreads)完全同期だがメインスレッドをフリーズさせる特殊な並列計算処理のみ

基本的には node:timers/promises を選択すれば間違いありません。

実践的なユースケース:リトライ処理の実装

スリープが最も活躍するのは、ネットワーク通信などでエラーが発生した際のリトライ(再試行)処理です。

一定時間の間隔(バックオフ)を空けずにリトライを繰り返すと、相手側のサーバーに過度な負荷をかけてしまいます。

JavaScript
import { setTimeout } from 'node:timers/promises';

async function fetchDataWithRetry(url, retries = 3) {
  for (let i = 1; i <= retries; i++) {
    try {
      console.log(`リクエスト試行 ${i}回目...`);
      // 擬似的なフェッチ処理
      if (Math.random() > 0.7) return "成功データ";
      throw new Error("通信失敗");
    } catch (err) {
      if (i === retries) throw err;
      
      const waitTime = i * 1000; // 徐々に待ち時間を長くする
      console.log(`${waitTime}ms 待機して再試行します`);
      await setTimeout(waitTime);
    }
  }
}

fetchDataWithRetry("https://example.com/api").then(console.log).catch(console.error);

このように、ループ処理の中で await setTimeout() を呼び出すことで、スマートなリトライアルゴリズムを簡単に実装できます。

実装時の注意点とパフォーマンス

スリープ処理を実装する際には、いくつか注意すべき点があります。

まず、「非常に短いスリープ(1msなど)をループ内で大量に実行しない」ことが重要です。

Node.jsのタイマー精度はOSのスケジューラに依存するため、ミリ秒単位の厳密な精度は保証されません。

また、数千、数万もの非同期スリープを同時に走らせると、メモリ消費量が増大し、イベントループの負荷が高まる可能性があります。

さらに、Atomics.wait を使った完全同期的なスリープという手法も存在しますが、これはメインスレッドで使用するとUIやサーバーの応答が完全に止まるため、使用は避けてください。

あくまで「非同期的な待機」を行うことが、Node.jsにおける正しいスリープのあり方です。

まとめ

Node.jsにおけるスリープの実装は、言語の進化とともに大きく変化してきました。

2026年現在の開発においては、従来の setTimeout コールバックを直接使うのではなく、Promiseベースの処理が標準となっています。

特に、Node.jsの標準モジュールである node:timers/promises を利用した await setTimeout() の実装は、最も可読性が高く、かつ高機能です。

AbortControllerによるキャンセル処理も可能なこの手法は、堅牢なアプリケーション開発に欠かせない要素です。

今回紹介した手法を適切に使い分け、メインスレッドをブロックしないクリーンな非同期コードを記述しましょう。

正しいスリープの実装をマスターすることで、エラーに強く、メンテナンス性の高いNode.jsアプリケーションを実現できるはずです。