エラーの内容
Redisコマンド — SET、GET、HSET、PUBLISH、その他何でも — を実行すると、次のエラーが返ってきます:
(error) ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET allowed in this context
その接続はPub/Subサブスクライバーモードに固定されています。接続が一度SUBSCRIBEまたはPSUBSCRIBEを呼び出すと、Redisはその接続を厳しくロックします。このモードを抜けるまで、サブスクライバー関連のコマンドしか受け付けません。PUBLISHを含め、それ以外のすべてのコマンドがこのエラーを引き起こします。
なぜこのエラーが発生するのか
Pub/Subモードは接続単位で適用されます。クライアントがSUBSCRIBE channelを送信した瞬間、そのTCP接続は専用のリスニング状態に入ります。Redisが意図的に他のコマンドをブロックするのは、同じソケット上での書き込みや読み取りによる干渉なしにメッセージ配信を保証する必要があるためです。
よくあるミスのシナリオ:
- サブスクライブとパブリッシュ/書き込みの両方に同じRedisクライアントインスタンスを使い回している
- コネクションプールが、すでにサブスクライブ済みの接続を通常のコマンドに払い出してしまう
- サブスクライバー接続に対して
PUBLISHを呼び出す —PUBLISHはサブスクライバーコマンドではなく書き込みコマンドであるため、非常によくあるミス - 接続を再利用する前にサブスクライブを解除し忘れる
修正方法
サブスクライバー接続は必ず専用にしてください。これは絶対的なルールです。SUBSCRIBE/PSUBSCRIBE専用の接続を1つ用意し、PUBLISHを含むそれ以外のすべての操作には完全に別の接続を使用してください。
redis-cliでの修正
ターミナルを2つ開き、それぞれに役割を割り当てます:
# ターミナル1 — サブスクライバー専用
redis-cli
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
# ターミナル2 — パブリッシャー/通常コマンド用
redis-cli
127.0.0.1:6379> PUBLISH news "hello"
(integer) 1
127.0.0.1:6379> SET foo bar
OK
サブスクライバー側でPub/Subモードを抜けるには、Ctrl-Cを押すか、UNSUBSCRIBEを送信してください。Redis 6.2以降ではRESETも使用できます。
Pythonでの修正(redis-py)
redis-pyにはPubSubオブジェクトが付属しており、内部で独自の接続を管理します — 接続を誤って混在させないようにするための仕組みです:
import redis
import threading
r = redis.Redis(host='localhost', port=6379)
# サブスクライバー — 専用pubsubオブジェクト(独自の接続を持つ)
def message_handler(message):
print(f"Received: {message['data']}")
pubsub = r.pubsub()
pubsub.subscribe(**{'news': message_handler})
# バックグラウンドスレッドでサブスクライバーを実行
thread = pubsub.run_in_thread(sleep_time=0.01)
# メイン接続 — それ以外はrを使用
r.set('foo', 'bar') # OK — メイン接続を使用
r.publish('news', 'hello') # OK — pubsubではなくメイン接続を使用
thread.stop()
よくある間違い — 生の接続オブジェクトを直接使用するケース:
# これは問題なし — rとpubsubは別々のオブジェクト
pubsub = r.pubsub()
pubsub.subscribe('news')
r.set('foo', 'bar') # OK
# これは壊れる:
raw_conn = r.connection_pool.get_connection('_')
raw_conn.send_command('SUBSCRIBE', 'news')
raw_conn.send_command('SET', 'foo', 'bar') # ERR only (P)SUBSCRIBE...
Node.jsでの修正(ioredis)
サブスクライブ用とそれ以外用に、それぞれ別のクライアントを2つインスタンス化します:
const Redis = require('ioredis');
const subscriber = new Redis();
const publisher = new Redis();
subscriber.subscribe('news', (err, count) => {
if (err) throw err;
console.log(`Subscribed to ${count} channel(s)`);
});
subscriber.on('message', (channel, message) => {
console.log(`[${channel}] ${message}`);
});
// publisherがすべての書き込みとPUBLISH呼び出しを担当
publisher.publish('news', 'hello');
publisher.set('foo', 'bar');
Javaでの修正(Jedis)
JedisPool pool = new JedisPool("localhost", 6379);
// サブスクライバー — スレッドをブロックするため、専用接続が必要
new Thread(() -> {
try (Jedis jedis = pool.getResource()) {
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println(channel + ": " + message);
}
}, "news");
}
}).start();
// 通常コマンド — プールから別の接続を取得
try (Jedis jedis = pool.getResource()) {
jedis.publish("news", "hello");
jedis.set("foo", "bar");
}
スタックしたサブスクライバー接続のリセット(Redis 6.2以降)
Pub/Subモードでスタックした接続を再利用したいですか?RESETを使えば、ソケットを閉じて再度開く必要なく、即座に通常の状態に戻せます:
127.0.0.1:6379> SUBSCRIBE news
...
127.0.0.1:6379> RESET
+RESET
Redis 6.2より前のバージョンでは、まずすべてのチャンネルからUNSUBSCRIBEするか、接続を閉じて新しく開き直してください。
修正の確認
- サブスクライバーのロジックを実行し、メッセージをクリーンに受信できることを確認します。
- 別の接続で書き込みを実行します:
SET test okがOKを返すはずです。 - 同じ接続からパブリッシュします:
PUBLISH news "ping"はエラーではなく、整数値(サブスクライバー数)を返すはずです。 - サブスクライバーが実際にメッセージを受信することを確認します — 受信できれば、両方の接続は正常です。
# 2つのターミナルでの簡易動作確認
# ターミナル1
redis-cli SUBSCRIBE test-channel
# ターミナル2
redis-cli PUBLISH test-channel "works"
# 期待される結果: (integer) 1
# ターミナル1には以下が表示されるはずです:
# 1) "message"
# 2) "test-channel"
# 3) "works"
クイックリファレンス
サブスクライバー接続で許可されるコマンド:
SUBSCRIBE/UNSUBSCRIBE— チャンネルサブスクリプションの管理PSUBSCRIBE/PUNSUBSCRIBE— パターンサブスクリプションの管理PING— キープアライブチェック(通常の+PONGではなく、特別なPub/Sub用pongを返します)QUIT— 接続を閉じるRESET— Pub/Subモードを抜ける(Redis 6.2以降)
それ以外のすべて — GET、SET、PUBLISH、HSET、EXPIRE、これらすべて — は必ずサブスクライバー以外の接続を通じて実行してください。

