ヘルスチェックパスは /_stcore/health
いきなり結論ですが、Streamlit を ALB の後ろに置くなら、ターゲットグループのヘルスチェックパスは /_stcore/health です。
GET すると、プレーンテキストで ok の2文字だけが返ってきます。
/health や /healthz を指定している例もよく見かけます。
どちらも今の Streamlit には存在しないパスですが HTTP 200 が返ります。
これに気づいたのは、認証まわりの検証用に Streamlit の疑似アプリを立てていたときでした。
疎通確認のつもりで /health を curl したら、200 と一緒にトップページの HTML が返ってきました。
ステータスコードだけ見ていたら、「通った」と思って先へ進んでいたはずです。
/_stcore/health でわかるのは、Streamlit のサーバとランタイムがブラウザからの接続を受け付けられるかまでです。スクリプトが import で失敗していても、DB に繋がらなくても ok は返ります。
この記事で防ぐのは、存在しないパスを監視して healthy だと思いこんでしまうことです。
検証環境は Streamlit 1.64.0 の素の streamlit hello(port 8501)、実測日は 2026-10-01 です。Streamlit の認証まわりと、アプリ本体の実装・デプロイ手順は扱いません。
/health が healthy に見えるのは、ALB がステータスコードしか見ないから
ALB のヘルスチェックは、ターゲットに GET を送り、返ってきたステータスコードを成功コードの設定と突き合わせるだけです。
この設定は API では Matcher、コンソールでは「成功コード」と呼ばれ、デフォルトは 200 です。ボディに何が書いてあるかは判定に使われません。
Streamlit は知らないパスにも 200 を返すので、 /health を指定したターゲットは、何事もなく healthy に並びます。
では「/health にするとアプリが死んでも永久に healthy」なのかというと、そこまで単純でもありません。エラー内容で結果が変わります。
| エラー内容 | /_stcore/health |
/health |
|---|---|---|
| プロセスが落ちた(クラッシュ・OOM) | 接続失敗で unhealthy | 接続失敗で unhealthy |
| プロセスは生きているが、ランタイムが準備できていない(起動直後・終了処理中) |
ok 以外が返り unhealthy |
200 + HTML で healthy(偽陽性) |
| ランタイムは応答できるが、スクリプトが異常(import エラー・DB 断) |
ok で healthy |
200 + HTML で healthy |
プロセスごと落ちた場合は、どのパスでも接続できないので切り離されます。差が出るのは2行目です。プロセスはまだいるのに、ブラウザからの接続をまだ受けられない、あるいはもう受けられない。起動直後や終了処理中のこの窓で、/health は 200 を返し続けます。
2行目の /_stcore/health 列は、Streamlit 側のヘルスチェックの定義から整理したものです。起動直後や終了処理中に /_stcore/health が実際に返すステータスコードとボディは確かめていません。
3行目は、正しいパスにしても拾えません。/_stcore/health が ok を返す条件は「Streamlit のサーバとランタイムがブラウザ接続を受け付けられる状態か」までです。この定義は旧 /healthz の頃のものをそのまま引き継いでいます(Streamlit forum)。スクリプトが import で例外を投げていても、DB に繋がらなくても、サーバが生きていれば ok です。
4本叩いて、4本とも 200 だった
streamlit hello に対して4つのパスを叩いた結果です。
| パス | ステータスコード | ボディ | 判定 |
|---|---|---|---|
/_stcore/health |
200 |
ok(プレーンテキスト) |
本物のヘルスチェック |
/health |
200 | HTML(index.html) | フォールバック |
/healthz |
200 | HTML | フォールバック(旧バージョンの名残) |
/this-does-not-exist |
200 | HTML | フォールバック |
コードは全部 200 でした。
ok を返したのは /_stcore/health だけで、区別はボディでしかつきません。
/healthz の例がネットに多いのは、それがかつての正解だったからです。
Streamlit は 1.14.0(2022年10月頃)でヘルスチェックを _stcore/ 配下へ移しました。*z で終わるパスが Google App Engine などの予約名とぶつかるのが理由だと、1.14.0 のリリース告知にあります。
1.14 より前のバージョンなら /healthz が実体を持つので、まず自分のバージョンを確かめてください。
なぜ存在しないパスが 200 を返すのか
Streamlit の画面は、ブラウザ上の JavaScript が描いています。
サーバが最初に返すのは index.html の1枚だけで、あとは JavaScript が WebSocket でサーバとやり取りしながら画面を組み立てていく、SPA(Single Page Application)と呼ばれる作りです。
SPA では、URL のパスごとに何を表示するかもブラウザ側が決めます。そのためサーバ側は、知らないパスが来ても 404 にせず、とりあえず index.html を 200 で返す作りになりがちです。この挙動がフォールバックです。/health は Streamlit にとって知らないパスなので、ここに落ちて HTML が返ります。
一方 /_stcore/ は Streamlit core が内部用に予約しているプレフィックスで、サーバ側に本物のハンドラがあるため、知らないパスと違ってフォールバックには落ちません。
手元なら streamlit hello と curl で再現できます
アプリを書く必要はありません。同梱のデモで足ります。
# venv などに streamlit を入れて起動(認証もアプリ本体も不要)
streamlit hello --server.port 8501 --server.headless true
別のターミナルから、ステータスコードではなくボディを見ます。
curl -s http://localhost:8501/_stcore/health
# ok
curl -s http://localhost:8501/health
# HTML が返る(ヘルスチェックではない)
# 存在しないパスも 200 になることの確認(フォールバックの証拠)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8501/this-does-not-exist
# 200
効いてくるのは最後の1本で、存在しないはずのパスでも 200 が出るなら、その環境ではステータスコードを判定材料にできないと分かります。
対策は ALB でパスと成功コードまで、ボディは別の層で見る
ALB 側でやることは、ヘルスチェックパスを /_stcore/health にすることと、成功コードをデフォルトの 200 から広げないことの2つです。
成功コードは、ALB なら 200〜499 の範囲で単一値・複数値・レンジを指定できます。よく見かける 200-399 にしても、Streamlit の偽陽性はきっちり 200 で返ってくるので防げません。3xx のリダイレクトまで healthy に数える分、むしろ緩くなります。4xx や 5xx はこのレンジに入らないので、エラー応答まで通るわけではありません。いちばん厳しいのは 200 のままです。
成功コードの設定でボディまで縛れないか、と考えたくなるところです。しかし Matcher に書けるのは HTTP コード(HttpCode)か gRPC コード(GrpcCode)だけで、ボディを判定する項目はありません。ALB で厳密にできるのはコードまでです。 ボディを確かめたいなら、ALB の外に仕組みを足します。
ECS で動かしているなら、タスク定義のコンテナヘルスチェックが使えます。コンテナの中でコマンドを実行し、終了コード 0 なら成功とみなす仕組みなので、ボディが ok かどうかを自分で比べられます。
"healthCheck": {
"command": [
"CMD-SHELL",
"python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8501/_stcore/health').read()==b'ok' else 1)\""
],
"interval": 30,
"timeout": 5,
"retries": 3
}
ここで curl を使わなかったのには理由があります。python:*-slim 系のベースイメージには curl が入っておらず、CMD-SHELL でそのまま呼ぶと curl: not found で UNHEALTHY になるからです。apt-get install で足す手もあります。ただ、Streamlit が動いている以上 Python は必ずあるので、依存を増やさないほうを選びました。
外から見たいなら、Route 53 ヘルスチェックの文字列マッチ(HTTP_STR_MATCH / HTTPS_STR_MATCH)が候補です。ステータスコードが 2xx か 3xx であることに加え、指定した文字列がレスポンスボディの先頭 5,120 バイトの中に含まれているかを見ます。
スクリプトが DB に繋がっているかまで知りたい場合、/_stcore/health をどこから叩いても届きません。アプリ側にヘルス確認用の処理を用意して、それを上の2つで検査する形になります。
他の SPA でも同じことが起きる
この落とし穴は Streamlit に限りません。React などの SPA を配信するサーバも、未知のパスに 200 と HTML を返す作りがよくあります。
初めて触るアプリでヘルスチェックを組むときは、存在しないはずのパスを1本 curl してみてください。それも 200 なら、そのアプリではステータスコードを当てにせず、ボディで判定する側に寄せます。
baseUrlPath を設定するとヘルスチェックパスも一緒に動く
Streamlit で baseUrlPath を設定すると、ヘルスチェックパスもその配下に移ります。--server.baseUrlPath st で起動して測った結果がこれです。
| パス | ステータスコード | ボディ |
|---|---|---|
/st/_stcore/health |
200 | ok |
/_stcore/health |
404 | Not Found |
/ |
404 | Not Found |
/st/health |
200 | HTML(フォールバック) |
前半で書いたフォールバックが効くのは、baseUrlPath の配下(/st/...)だけです。配下にある /st/health は HTML で 200 になりますが、配下の外に出た /_stcore/health や / は 404 が返ります。
baseUrlPath を足したのに ALB 側を /_stcore/health のまま残すと、404 で unhealthy になり、ターゲットが切り離されます。偽陽性とは逆向きの壊れ方ですが、ターゲットがいきなり全部落ちてすぐ表に出る分、こちらは気づきやすいほうです。
パスベースルーティングで baseUrlPath が必要なら、ヘルスチェックパスを /st/_stcore/health に合わせます。ホスト名で振り分けられるなら、設定しないままにしておくほうが直す箇所が減り、事故も起きにくいです。
まとめ
ALB にとっての 200 は「何かが返ってきた」以上の意味を持ちません。トップページの HTML でも同じ 200 になる以上、パスを間違えても ALB は何も教えてくれません。
ヘルスチェックを組んだら、正しいパスのボディと、存在しないパスのステータスコードを1回ずつ curl で見ておいてください。
その2本で、設定したヘルスチェックが本当にアプリの状態を見ているかがわかります。