VPSSPark ブログ
← 開発日記に戻る

サーバー帯域ボトルネックを解消する:Nginx リバースプロキシ調整の実践

サーバーメモ · 2026.07.22 · 読了目安 約14分

よくある検索:Nginx リバースプロキシ チューニング · サーバー帯域最適化 · gzip · proxy_cache

サーバーラックとネットワークスイッチ—Nginx リバースプロキシと帯域最適化
帯域ボトルネックは CPU 不足ではなく、Nginx がバイトを無駄にしていることが多い。

先月、SaaS バックエンドを運用しているお客様のトラブルシュートを手伝いました。2 コア 4 GB の VPS で、CPU は常時 20% 前後なのに、毎日午後 3 時になるとサイトがスライドショーのように重くなる。クラウドの監視を見るとアウトバウンド帯域が上限まで張り付いたまま、100 Mbps プランが 40 分間フル稼働。最初の反応は「スペックアップかな」——Nginx のアクセスログを開くと、トラフィックの 70% が未圧縮の JSON API レスポンスと、?v= 付きランダムパラメータの静的 JS。同じリソースが毎回フルサイズで配信されていました。

Gzip、静的キャッシュヘッダー、upstream keepalive を調整したあと、ピーク帯域は 35 Mbps まで下がり、ページ読み込みは 8 秒から 1.2 秒に。追加費用ゼロです。中小 VPS ではこのパターンが本当によくある。ボトルネックは CPU ではなく出口帯域。Nginx をリバースプロキシとしてユーザーとバックエンドの間に置けば、「帯域を節約する」最強のレバーになる——ただし、どのネジを回すか知っていることが前提です。

本記事は Nginx のインストール入門ではなく、本番で検証してきた帯域重視のチューニングチェックリストをまとめたものです。診断 → 圧縮 → キャッシュ → 接続再利用の順で進めます。443 リバースプロキシは既に構築済みという前提(ファイアウォールがまだなら、先に クラウドサーバー UFW ファイアウォール設定ガイド で 22/80/443 だけ開いてから最適化へ)。

約 65%
Gzip 後の JSON サイズ典型削減率
4 ステップ
診断→圧縮→キャッシュ→接続
0 円
多くの最適化はスペックアップ不要

なぜ CPU より先に帯域が枯れるのか

クラウド VPS の帯域課金は非対称なことが多い。100 Mbps ピーク・月間 1〜2 TB のパッケージでも、未圧縮の 500 KB JSON を秒間 200 リクエストすれば理論上 800 Mbps——一瞬で上限突破。一方 Nginx の静的配信やシンプルなリバースプロキシは CPU 負荷が低く、監視グラフでは「CPU は暇、帯域は満杯」というズレが出やすい。

もう一つの落とし穴は、すべてのトラフィックをオリジンに直撃させること。CDN なし、ブラウザキャッシュなし、proxy_cache なし——ユーザーがリロードするたびに VPS から bundle.js と logo.png を丸ごと再取得。Nginx 公式ドキュメントの http モジュールは、本質的に「バイトを減らす」「接続を減らす」ツールボックス。万能設定をコピペするのではなく、シーンに合わせて組み合わせるのがコツです。

Nginx リバースプロキシ帯域最適化の 4 ステップ:診断、圧縮、キャッシュ、接続再利用
まずコストゼロ(gzip、expires、keepalive)から。それでも足りなければ CDN や帯域アップグレードを検討。
症状 よくある原因 優先アクション
API が遅い・帯域が高い JSON/HTML 未圧縮 gzip / brotli を有効化
リロードでトラフィック増 静的リソースに Cache-Control なし expires + フィンガープリントファイル名
バックエンド接続数が急増 リクエストごとに upstream TCP 新規作成 keepalive 接続プール
大容量ダウンロードで出口が飽和 オリジン直送・分割キャッシュなし CDN または proxy_cache

ステップ 1:まず診断。いきなり帯域アップは禁物

設定をいじる前に 10 分だけ、「帯域がどこに消えているか」を確認しましょう。サーバー上ではこのコマンドセットが定番です。

帯域・接続の診断
# 接続単位でリアルタイム流量(apt install iftop が必要)
                sudo iftop -i eth0

                # 時間・日単位の統計(vnstat)
                vnstat -h
                vnstat -d

                # Nginx アクセスログ:ボリュームの大きい URL を特定
                awk '{print $7, $10}' /var/log/nginx/access.log | \
                  awk '{a[$1]+=$2} END {for(i in a) print a[i], i}' | sort -rn | head -20

                # 現在の ESTABLISHED 接続数
                ss -s
                ss -tn state established | wc -l

ログ上位が /api/ でレスポンスが数百 KB 単位なら、圧縮が最優先。.js.css.woff2 がすべて 200(304 なし)ならキャッシュヘッダー未設定のサインです。インバウンドアウトバウンドも切り分けてください。DDoS やクローラーでインバウンドが飽和するのは別問題(レート制限、WAF、fail2ban)。本記事は正常な業務トラフィックのアウトバウンド最適化にフォーカスします。

クラウドコンソールの帯域グラフは 1〜5 分の集約遅延があります。トラブルシュート時はサーバー上の iftop を正としましょう。SSH 自体がカクつく場合は、Linux サーバー SSH 接続拒否のトラブルシューティングでネットワーク層の障害を先に除外してください。

ステップ 2:Gzip / Brotli 圧縮——最もコスパの良い帯域削減

テキスト系レスポンス(JSON、HTML、JS、CSS、SVG、XML)の圧縮率は通常 60%〜80%。Nginx には gzip モジュールが組み込まれており、数行の設定で有効化できます。多くの VPS では CPU オーバーヘッドも許容範囲内です。

/etc/nginx/nginx.conf または conf.d/gzip.conf
gzip on;
                gzip_vary on;
                gzip_proxied any;
                gzip_comp_level 5;          # 6–9 は収益逓減、5 がコスパのスイートスポット
                gzip_min_length 256;        # 小さすぎるレスポンスは圧縮不要
                gzip_types
                    text/plain
                    text/css
                    text/javascript
                    application/javascript
                    application/json
                    application/xml
                    image/svg+xml
                    font/woff2;

実戦のポイント:gzip_proxied any でバックエンド経由のレスポンスも圧縮対象に(デフォルトは直結のみ)。JPEG/PNG/WebP や動画は gzip 不要——すでに圧縮済みのバイナリにかけると CPU の無駄遣い。確認は curl -H 'Accept-Encoding: gzip' -I https://your.site/api/fooContent-Encoding: gzip があるか。

Brotli モジュール付きビルドなら、同等品質で gzip より 15%〜20% さらに削減可能。テキスト API 中心のサイト向き。モジュールがなければ gzip を安定させるだけで十分です。

HTTPS とレガシークライアントに注意
ごく一部の古いクライアントは HTTPS 上の gzip レスポンスを正しく処理できません。現代のブラウザでは問題なし。極端に古い組み込み機器向け API なら特定 User-Agent で gzip を切る手もありますが、2026 年の大半のシーンでは気にする必要はありません。

ステップ 3:静的リソースのキャッシュと sendfile

Vite や Webpack はファイル名に content hash を付けます(例:app.a3f2b1.js)。こういうファイルは長期キャッシュ可能で、2 回目以降のアクセスではネットワークを通りません。よくあるのはビルド側だけ hash を付けて、Nginx 側で Cache-Control を出していないケース——ブラウザは毎回フルリクエストを送り続けます。

静的リソース location の例
location ~* \.(js|css|woff2?|ttf|ico|svg)$ {
                    root /var/www/app/dist;
                    expires 30d;
                    add_header Cache-Control "public, immutable";
                    access_log off;           # 静的ヒット時は access_log オフで IO 削減
                }

                # http ブロックで推奨
                sendfile on;
                tcp_nopush on;
                tcp_nodelay on;

sendfile はカーネルがファイルページキャッシュをソケットに直接送り、ユーザ空間のコピーを減らします。大きな静的ファイルや高並行ダウンロードに効きます。AIO やスレッドプールと組み合わせる場合は要検証。デフォルトの trio(sendfile + tcp_nopush + tcp_nodelay)で大半のサイトは十分です。

HTML と index.html には immutable を付けないでください。デプロイ後にユーザーが古いシェルを見る可能性があります。hash 付きリソースは 30 日、HTML は no-cache(協調キャッシュ可)が定番です。

ステップ 4:リバースプロキシキャッシュ(proxy_cache)

読み取り中心の API(設定一覧、商品カタログ、記事詳細)なら、Nginx 層で短い TTL キャッシュを置けば、オリジン帯域はヒット率で割り算できます。proxy モジュールで最も過小評価されがちな機能のひとつです。

proxy_cache 基本設定
# http ブロック
                proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=apicache:50m
                                 max_size=2g inactive=60m use_temp_path=off;

                upstream backend {
                    server 127.0.0.1:3000;
                    keepalive 32;              # 次のセクション参照
                }

                server {
                    location /api/ {
                        proxy_pass http://backend;
                        proxy_http_version 1.1;
                        proxy_set_header Connection "";

                        proxy_cache apicache;
                        proxy_cache_valid 200 5m;
                        proxy_cache_key "$scheme$request_method$host$request_uri";
                        add_header X-Cache-Status $upstream_cache_status;

                        proxy_buffering on;
                        proxy_buffers 16 16k;
                        proxy_busy_buffers_size 64k;
                    }
                }

$upstream_cache_statusHITMISSBYPASS を返すので、キャッシュが効いているか一目瞭然。書き込み(POST/PUT/DELETE)は必ずバイパス。Cookie 付きのパーソナライズ API は慎重に——A ユーザーのデータを B に返す事故を防ぐため。

proxy_buffering on では Nginx がバックエンドレスポンスをバッファに溜めてからクライアントへ送ります。バックエンドが遅くクライアントが速いとき、バックエンド接続の長時間占有を防げます。SSE ストリーミングや大容量アップロード向け location では proxy_buffering off を検討してください。

ステップ 5:upstream keepalive で TCP ハンドシェイクを削減

デフォルトでは、クライアントリクエストごとにバックエンドへの新規 TCP 接続が張られることがあります。高 QPS では TIME_WAIT と三次握手が CPU とローカルポートを食い、間接的に帯域効率も落ちます。upstream keepalive で接続プールを維持し、リクエスト完了後に接続を返却・再利用します。

keepalive 必須 3 点セット
upstream backend {
                    server 127.0.0.1:8080;
                    keepalive 64;               # プールサイズ、QPS に応じて調整
                }

                location / {
                    proxy_pass http://backend;
                    proxy_http_version 1.1;     # HTTP/1.1 でないと keepalive 不可
                    proxy_set_header Connection "";  # Connection: close をクリア
                }

proxy_http_version 1.1Connection "" のどちらかが欠けると keepalive は効きません——現場でいちばん多い「設定したのに効いてない」パターンです。プールサイズ keepalive 64 は日 PV 百万未満なら大抵十分。大きすぎるとバックエンドのファイルディスクリプタを圧迫します。

ステップ 6:worker、接続数、レート制限

帯域が飽和しているとき、Nginx 自体が接続数のボトルネックになっていないかも確認します。主要パラメータは nginx.conf トップレベルにあります。

worker と接続(2 コア VPS の目安)
worker_processes auto;
                worker_rlimit_nofile 65535;

                events {
                    worker_connections 4096;
                    use epoll;
                    multi_accept on;
                }

                # 任意:IP 単位のレート制限、単一ユーザーによる帯域占有防止
                limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
                limit_conn_zone $binary_remote_addr zone=conn:10m;

                server {
                    limit_req zone=perip burst=20 nodelay;
                    limit_conn conn 50;
                }

worker_connections × worker_processes はピーク同時接続数より大きく。2 コアなら worker_processes auto は通常 2、4096 × 2 = 8192 で中小サイトは余裕です。システムの fs.file-maxworker_rlimit_nofile も揃えないと、ログに too many open files が出ます。

limit_rate は単一接続の帯域制限(大容量ダウンロード向け)、limit_req は API 乱打防止。帯域を「増やす」わけではありませんが、異常トラフィックが全体を巻き込むのを防ぐ——帯域ガバナンスの最後の砦です。

ステップ 7:CDN を入れるべきタイミングは?

最適化を終えても帯域が周期的に上限に張り付くなら、CDN かプランアップグレードの検討時です。判断基準はシンプルです。

  • 静的リソースがアウトバウンドの 60% 以上 → CDN で JS/CSS/画像をキャッシュ、オリジンは HTML と API のみ
  • ユーザーが複数地域に分散 → エッジノードで物理距離を短縮、オリジン帯域圧力を実質軽減
  • キャンペーンやライブ配信など突発トラフィック → CDN がピークを吸収、ピーク帯域でプランを買わずに済む

CDN 導入後の Nginx 側:オリジンは CDN 回源 IP のみ受け入れ(ファイアウォールホワイトリスト)、実クライアント IP は透過(X-Forwarded-For / real_ip モジュール)。さもないと攻撃者がオリジン IP に直アクセスし、CDN の防御とキャッシュをバイパスされます。

完成 server ブロック例(ドメインを差し替えてそのまま使える)

上記の設定を 1 本にまとめた最小構成。Node / Python バックエンドが 127.0.0.1:3000 で待ち受けている想定です。

/etc/nginx/sites-available/app.conf
upstream app {
                    server 127.0.0.1:3000;
                    keepalive 32;
                }

                server {
                    listen 443 ssl http2;
                    server_name app.example.com;

                    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
                    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

                    gzip on;
                    gzip_types application/json application/javascript text/css text/plain;
                    gzip_min_length 256;

                    location /assets/ {
                        alias /var/www/app/dist/assets/;
                        expires 30d;
                        add_header Cache-Control "public, immutable";
                        access_log off;
                    }

                    location / {
                        proxy_pass http://app;
                        proxy_http_version 1.1;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_set_header X-Forwarded-Proto $scheme;
                        proxy_set_header Connection "";
                    }
                }

変更後は sudo nginx -t && sudo systemctl reload nginx。ブラウザの開発者ツール Network で静的リソースに 30 日キャッシュ、API に gzip があるか確認。デプロイチェックリストには curl -sI https://app.example.com/assets/main.js | grep -i cache を入れる習慣があります。

検証と継続モニタリング

チューニングは一度きりではありません。Grafana / クラウド監視でこの 4 本を同時に見ましょう:アウトバウンド帯域、Nginx アクティブ接続数、upstream レスポンスタイム、キャッシュヒット率(proxy_cache 有効時)。リリースやキャンペーン前にベンチマーク比較:ab -n 1000 -c 50 https://app.example.com/hey -n 5000 -c 100 で、調整前後の帯域ピークと P95 レイテンシを記録。

帯域は下がったのにレイテンシが高いなら、ボトルネックは DB やディスク I/O に移った可能性があります——別の話題です。ただ、盲目に 500 Mbps プランを買うよりインデックスを足すべきかは、ここで切り分けられます。

ひとことまとめ
まず iftop で大口を特定し、gzip でテキストを圧縮、expires で静的をキャッシュ、keepalive でバックエンド接続を再利用。読み取り中心 API には proxy_cache。それでも足りなければ CDN。多くの VPS 帯域問題は、ハードではなく設定の問題です。

チューニングの先:ノード選びも帯域を左右する

Nginx で「送るバイト」を減らしても、ユーザーとオリジン間の物理距離は設定では消せません。アジア太平洋のユーザー向けなのに米東 VPS だと、RTT が高く再送が増え、実効スループットは最初から割り引かれます。ビルドとプレビューをクラウド Mac やユーザーに近いノードに置き、静的はオブジェクトストレージ + CDN、オリジンは API だけ——帯域請求がだいぶマシになります。

VPSSpark の Mac mini M4 クラウドノードは CI ビルド、内網プレビュー、踏み台運用向き。macOS ネイティブ環境で Terminal と OpenSSH がそのまま使え、待機消費電力は約 4W。7×24 の中継やビルドマシンに向き、x86 小ホストをもう一台立てるより省電力です。UFW で 443 のみ、アプリは 127.0.0.1 バインド——攻撃面と帯域の無駄遣いを同時に抑えられます。

「帯域を節約し、運用しやすい」デプロイ構成を考えているなら、 VPSSPark クラウド Mac mini は低消費電力の踏み台・ビルドノードとして現実的な選択—— プランを今すぐ確認 、Nginx チューニングの成果をノード選びで台無しにしないために。

期間限定

帯域を最適化したら、ノードも追いつかせよう

低消費電力 Mac mini · ネイティブ Unix 踏み台 · 24/7 静音稼働

ホームへ
期間限定 プランを見る