Redisの「BUSY Redis is busy running a script」エラーの修正方法

intermediate🔴 Redis2026-04-16| Redis 2.6+、任意のOS(Linux、macOS、Windows WSL)、任意のRedisクライアント(redis-cli、Python redis-py、Node ioredis など)

Error Message

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
#redis#lua#script#パフォーマンス#busy#ブロッキング

エラーの内容

Redisに対してコマンドを実行したところ、以下のエラーが返ってきました:

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

RedisがLuaスクリプトの実行中でフリーズしています。そのスクリプトが完了するか強制終了されるまで、送信した他のすべてのコマンドは拒否されます。Redisはシングルスレッドで動作するため、暴走したスクリプト1つでサーバー全体が人質に取られてしまいます。

発生する原因

RedisはEVALまたはEVALSHAを通じてLuaスクリプトをアトミックに実行します。このアトミック性こそが本来の目的であり、スクリプトの実行中は他のコマンドが割り込むことができません。その代償は深刻で、実行時間が長すぎるスクリプトはRedisインスタンス全体をフリーズさせてしまいます。

デフォルトのタイムアウトは5000mslua-time-limit)です。この閾値を超えると、RedisはSCRIPT KILLSHUTDOWN NOSAVE以外のすべてのコマンドを受け付けなくなります。スクリプトがタイムアウトを超えてしまう主な原因は以下の通りです:

  • 無限ループや上限のないイテレーション — 例えば、1つのEVAL呼び出しの中で1,000万件のキーをスキャンするケース
  • 終了条件のないredis.call()のループ
  • 開発環境では500件のキーで問題なく動いていたスクリプトが、本番環境では500万件に当たってしまうケース
  • ループの終了条件(カーソルが0に到達する)が永遠に来ないレースコンディション

手順を追った解決方法

手順1:実行中のスクリプトを強制終了する

新しいターミナルを開いてください — 現在の接続はおそらくハングしています。Redisに接続します:

redis-cli -h 127.0.0.1 -p 6379

次のコマンドを実行します:

SCRIPT KILL

スクリプトがまだ何も書き込んでいなければ、Redisは即座に終了させます:

OK

完了です。サーバーのブロックが解除され、通常のコマンドを受け付けるようになります。

手順2:SCRIPT KILLがエラーを返す場合

代わりにこのエラーが出た場合はどうすればよいでしょうか?

(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait it to finish or kill the server in a non destructive way.

Redisが中途半端な書き込み状態からデータを保護しています。すでに書き込みを行ったスクリプトは、実行途中で安全に中断することができません。対処方法は2つあります:

  • 完了を待つ — スクリプトが処理を進めており最終的に終了する見込みがある場合は、待ちましょう。redis-cli --latencyでステータスを確認するか、Redisのログを監視してください。
  • Redisを再起動する — 本当に無限ループにはまっている場合は、再起動しか選択肢がありません。適切な方法を選んでください:
# Linux systemd
sudo systemctl restart redis

# macOS Homebrew
brew services restart redis

# Direct process
sudo service redis-server restart

**SHUTDOWN NOSAVEについて:**このコマンドはサーバーを強制終了し、最後のRDB/AOF保存以降に書き込まれた内容をすべて破棄します。メモリ上の変更を失っても問題ないと確信できる場合にのみ使用してください。

手順3:問題のあるスクリプトを特定する

サーバーが応答できるようになったら、BUSYの状態を引き起こした原因を調査します。スローログを取得しましょう:

redis-cli SLOWLOG GET 10

出力の中からEVALまたはEVALSHAのエントリを探してください。実行時間が5,000,000マイクロ秒(5秒)以上のものが原因です。アプリケーションのログと照合して、どのコードパスが引き起こしたかを特定してください。

手順4:スクリプトを修正する

BUSYエラーのほとんどは同じ根本原因を持っています:1つのLuaスクリプトに詰め込みすぎているのです。よくあるパターンの修正方法を紹介します。

上限のないスキャンを分割する:

-- BAD: iterates until cursor == 0, no upper bound
local cursor = 0
repeat
  local result = redis.call('SCAN', cursor, 'MATCH', 'prefix:*')
  cursor = tonumber(result[1])
  -- process keys...
until cursor == 0

-- BETTER: move the scan loop to your application layer entirely
-- Don't do full key scans inside a single EVAL call

重い処理をアプリケーションコードに移す:

# Python redis-py — scan in batches of 100
import redis

r = redis.Redis()
cursor = 0
while True:
    cursor, keys = r.scan(cursor, match='prefix:*', count=100)
    for key in keys:
        # process key
        pass
    if cursor == 0:
        break

Luaスクリプトが真価を発揮するのは、少数の限定されたキーに対するアトミックな読み取り・変更・書き込みです。バッチ処理やスキャン、反復回数がデータ量に依存する処理はアプリケーション側に戻しましょう。

手順5:lua-time-limitを調整する(任意)

デフォルトの5秒は、Redisがコマンドを拒否し始めるまでの猶予として妥当な時間です。redis.confで変更できます:

# redis.conf
lua-time-limit 5000   # milliseconds

# Or change it live without a restart:
redis-cli CONFIG SET lua-time-limit 5000

この値を上げても問題を先送りにするだけです。実行時間の上限が既知で、レビュー済みの特定のスクリプトがある場合にのみ引き上げてください — 監査していないループへの応急処置として使うべきではありません。

修正の確認

スクリプトを強制終了またはサーバーを再起動した後、簡単な動作確認を行いましょう:

redis-cli PING
# Expected: PONG

redis-cli INFO server | grep redis_version
# Should return version info without hanging

続いて、読み取りと書き込みの両方が機能することを確認します:

redis-cli SET test-key "hello"
# OK
redis-cli GET test-key
# "hello"
redis-cli DEL test-key

3つすべてが即座に応答すれば、問題は解決しています。

再発防止策

  • **本番環境と同規模のデータでテストする。**100件のキーをスキャンするスクリプトはマイクロ秒で終わります。しかし同じスクリプトが1,000万件に当たると、5秒を簡単に超えてしまいます — そしてそれは手遅れになるまでわかりません。
  • クライアント側のタイムアウトを設定することで、アプリがハングしたEVALを無限に待ち続けるのを防げます:
# Python redis-py
r = redis.Redis(socket_timeout=5.0)

# Node ioredis
const client = new Redis({ commandTimeout: 5000 });
  • **Luaの中で上限のないループを使わない。**反復回数がデータ量に依存する場合は、アプリケーションコード側でバッチ処理したSCAN呼び出しで対応してください。
  • 本番環境でスローログを監視する。EVALが1〜2秒を超えたらアラートを出すようにしましょう — スクリプトの遅延を2秒で捕捉できれば、6秒でサーバーがフリーズする事態を未然に防げます。
  • **Redis 7.0以降では、EVALの代わりにFUNCTIONの利用を検討してください。**FunctionはNo-writesフラグをサポートしており、実行開始後でもSCRIPT KILLが機能するようになります — UNKILLABLEの状況を完全になくすことができます。

Related Error Notes