投稿日:2026/8/9
更新日:2026/8/9

perform_async を呼んだあと、ジョブは Redis の中でどんな形で持たれているのか。
Sidekiq は「全部まとめて1個のキュー」ではなく、用途ごとに Redis のデータ構造を使い分けている。
その対応関係を整理する。
LPUSH(左端)、取り出しが BRPOP(右端)で FIFO になる。flowchart LR
App["Rails アプリ"]
subgraph Redis["Redis"]
Q["queue:default<br/>(List)"]
S["schedule<br/>(Sorted Set)"]
R["retry<br/>(Sorted Set)"]
D["dead<br/>(Sorted Set)"]
P["processes<br/>(Set) / プロセス情報(Hash)"]
end
Worker["Sidekiq プロセス"]
App -->|"perform_async → LPUSH"| Q
App -->|"perform_in / perform_at → ZADD"| S
S -->|"時刻到来したら Poller が移動"| Q
Q -->|BRPOP| Worker
Worker -->|"失敗 → ZADD"| R
R -->|"再実行時刻が来たら移動"| Q
R -->|"リトライ上限"| D
Worker -->|"稼働状況を書き込み"| P
WelcomeEmailJob.perform_async(123, "hoge") を呼ぶと、Redis に積まれるのは次のような1本の JSON 文字列。
{
"class": "WelcomeEmailJob",
"args": [123, "hoge"],
"retry": true,
"queue": "default",
"jid": "a1b2c3d4e5f6",
"created_at": 1691234567.123,
"enqueued_at": 1691234567.456
}
| キー | 意味 |
|---|---|
class |
実行するジョブクラス名。ワーカー側はこの文字列から const_get してインスタンス化する |
args |
perform に渡される引数。JSON で表現できる値だけ |
retry |
失敗時のリトライ設定(true / false / 回数) |
queue |
投入先のキュー名 |
jid |
ジョブ一意 ID。ログ追跡やダッシュボードでの特定に使う |
created_at / enqueued_at |
作成時刻 / キュー投入時刻。予約ジョブだとこの2つがずれる |
ここが Sidekiq の一番の制約。ActiveRecord のモデルをそのまま渡せない。
argsは JSON にシリアライズされるので、渡せるのは String / Integer / Float / Boolean / nil / Array / Hash だけ。
ID を渡してワーカー側でfindする、が基本形になる理由がこれ。
(ActiveJob 経由の場合は GlobalID でモデルを渡せるが、これも「gid://app/User/123という文字列に変換している」だけで、
結局 Redis に入るのは文字列。実行時にはその ID で引き直されるので、状態が変わっている前提は変わらない)
queue:default のような通常のキューは Redis の List で管理される。List は挿入順を保持する配列のような構造。
| 側 | コマンド | 位置 |
|---|---|---|
| 投入(Rails) | LPUSH queue:default '{"class":...}' |
左端(先頭)に積む |
| 取得(Sidekiq) | BRPOP queue:default 2 |
右端(末尾)から取る |
左から入れて右から出すので、先に入れたジョブが先に出る(FIFO)。
BRPOP の B は Blocking。「ジョブが入るまで待つ」コマンドなので、Sidekiq は
「ジョブある?ジョブある?」とポーリングし続ける必要がない。ジョブが LPUSH された瞬間に
ブロックが解除され、即座に拾い上げる。
複数キューを優先度つきで見る場合も、BRPOP queue:critical queue:default 2 のように
引数を並べるだけで「critical を先に見る」が実現できる。
なお、存在するキュー名の一覧は別途 queues という Set に SADD されている。
ダッシュボードの「キュー一覧」はこれを読んでいる。
BRPOP は「取り出す」コマンドなので、ワーカーがジョブを受け取った時点で Redis 上には残っていない。
つまり 実行中に kill -9 などでプロセスが即死すると、そのジョブは失われる。
TERM)なら、Sidekiq は処理中のジョブを待ってから終了するので消えないkill -9、コンテナの強制停止では落ちるsuper_fetch は RPOPLPUSH で「絶対に落としたくないジョブ」は、ジョブ側を冪等にしたうえで、
DB 側にも実行予定レコードを持たせて突き合わせる、といった設計で担保することになる。
perform_in(1.hour, ...) のような予約実行や、失敗して再実行待ちのジョブは List ではなく
Sorted Set で持たれる。キー名はそれぞれ schedule と retry。
Sorted Set は、各要素に score(数値) を付けて自動でソートしてくれる構造。
Sidekiq はこの score に 「実行すべき時刻の UNIX タイムスタンプ」 を入れる。
ZADD schedule 1691238167.0 '{"class":"WelcomeEmailJob","args":[123],...}'
^^^^^^^^^^^^
実行したい時刻
こうしておくと「今すぐ実行すべきジョブ」を取り出す操作が、
「score が現在時刻以下の要素を取る」 という Redis の1コマンドで済む。
Sidekiq 内部のポーリングスレッド(Scheduled::Poller)が数秒おきにこれを実行している。
sequenceDiagram
participant App as Rails アプリ
participant SS as schedule / retry(Sorted Set)
participant Poller as Sidekiq Poller
participant Q as queue:default(List)
participant W as ワーカースレッド
App->>SS: ZADD schedule 実行時刻 ジョブJSON
Note over SS: 実行時刻順に並んだ状態で待機
loop 数秒おき
Poller->>SS: ZRANGE schedule -inf 現在時刻 BYSCORE
SS-->>Poller: 時刻が来たジョブ
Poller->>SS: ZREM(取れたものだけ)
Poller->>Q: LPUSH(通常キューへ移動)
end
Q->>W: BRPOP
W->>W: perform 実行
ZREM の戻り値で「自分が取った」ことを確認してから LPUSH するので、
Sidekiq プロセスが複数台いても同じジョブが二重に enqueue されない。
retry に積まれるジョブ JSON には、失敗の情報が追記される。
{
"class": "WelcomeEmailJob",
"args": [123],
"jid": "a1b2c3d4e5f6",
"retry_count": 2,
"failed_at": 1691234600.0,
"retried_at": 1691234900.0,
"error_class": "Net::OpenTimeout",
"error_message": "execution expired"
}
次のリトライ時刻は retry_count から指数的に伸びる形で決まり、その値がそのまま score になる。
リトライ上限に達したジョブは dead(これも Sorted Set)へ移され、
ダッシュボードの Dead 一覧に出る。
stateDiagram-v2
[*] --> scheduled : perform_in / perform_at
[*] --> enqueued : perform_async
scheduled --> enqueued : 実行時刻が来た
enqueued --> processing : BRPOP
processing --> [*] : 成功
processing --> retry : 例外を raise
retry --> enqueued : リトライ時刻が来た
retry --> dead : リトライ上限に到達
dead --> enqueued : 手動で Retry
ダッシュボードの「稼働中プロセス」「実行中ジョブ」の表示も、すべて Redis から読んでいる。
| 用途 | 型 | キー(おおよそ) |
|---|---|---|
| 稼働中の Sidekiq プロセス一覧 | Set | processes |
| 各プロセスの状態(起動時刻・並列数・busy 数など) | Hash | <ホスト名>:<PID>:<識別子> |
| そのプロセスが今処理しているジョブ | Hash | 上記キー + :work |
| 累計の処理数・失敗数 | String(カウンタ) | stat:processed / stat:failed |
各 Sidekiq プロセスは数秒おきに ハートビート を打っていて、そのたびに
自分の Hash を更新し、キーに TTL(60秒程度)を張り直している。
この TTL が「プロセスが死んだら一覧から自動で消える」を実現している。
ハートビートが止まればキーが期限切れで消滅し、ダッシュボードからも消える。
死活監視のための別テーブルもハートビート受信サーバも要らない、という設計。
「今どのスレッドが何のジョブを処理しているか」は :work の Hash に
スレッドID → ジョブ情報の JSON という形で入っている。
Hash なので「特定スレッドの情報だけ更新」「まとめて全部取得」の両方が安く済む。
| 用途 | 型 | なぜその型か |
|---|---|---|
| 即実行キュー | List | 順序を保持でき、BRPOP でブロッキング取得できる |
| 予約実行 / リトライ待ち | Sorted Set | score に実行時刻を入れれば「時刻が来たものだけ」を範囲取得できる |
| キュー名一覧 / プロセス一覧 | Set | 重複を持ちたくない集合。存在確認が O(1) |
| プロセス状態 / 実行中ジョブ | Hash | フィールド単位で読み書きしたい構造化データ |
| 処理数・失敗数 | String | INCR でアトミックに加算できるカウンタ |
Sidekiq が速いのは「Redis が速いから」だけではなく、
やりたい操作がそのまま Redis の1コマンドになるデータ構造を選んでいるから、という部分が大きい。
「時刻の来たジョブを探す」を SQL でやろうとすればWHERE run_at <= NOW() FOR UPDATE SKIP LOCKED 相当の仕組みが要るところを、
Sorted Set の範囲取得1発に落としている。
args を太らせない。 ジョブ JSON は Redis(=メモリ)に載る。perform_in(5.seconds, ...) はretry / dead が溜まるとメモリを食う。 Sorted Set に JSON が積み上がるので、dead の保持期間を放置しない。enqueued_at と created_at の差がキューの詰まり具合ではない。enqueued_at と現在時刻の差を見る。BRPOP 済みのジョブは Redis 上にない。