PR

FastAPIへ50MB送ると4GB VPSのRAMは足りる?UploadFileとRequest.streamを実測

【PR】この記事にはA8.netのアフィリエイト広告を含みます。

先に結論:4GBのXServer VPSへ50MiBの証拠ファイルを送る実験は、FastAPIUploadFileRequest.stream()の両方で3/3回成功しました。Request.stream()cgroupピークRAMは86.78MiBで、UploadFileの139.93MiBより53.15MiB少ない結果です。ただし50MiBの送信時間は164.957ms対153.864msで、streamの高速化は確認できませんでした。悩みの判定は一部解決です。

4GBのXServer VPSで50MBファイルをFastAPIへ送れる?

今回の直列・localhost条件では送れました。1、10、50MiBのbinary fileを2方式へ各3回、合計18回POSTし、すべてHTTP 200です。受信byte数、disk保存後のbyte数、SHA-256も18/18回一致しました。

これは将来、Android実機調査で作るスクリーンショット、操作ログ、短い画面録画などをVPSへ集める前の基礎実験です。GB級動画や同時uploadまで安全だと示す実験ではありません。

UploadFileとRequest.stream()は何が違う?

項目 UploadFile Request.stream()
送信形式 multipart/form-data raw body
一時保存 SpooledTemporaryFile 受信chunkを直接処理
filename 取得しやすい header/query等で別送が必要
今回の保存 1MiBずつdiskへ書く 受信chunkdiskへ書く
今回の整合性 保存中にSHA-256 保存中にSHA-256

FastAPI公式のRequest Filesでは、UploadFileは一定量までmemory、その後diskへ移るspooled fileを使うと説明されています。Starlette公式のRequest bodyでは、request.stream()body全体をmemoryへ保存せずchunkを受け取れると説明されています。

なぜ同じdisk保存・SHA-256までそろえた?

元の悩みは、ZennNishika Tech Blog【FastAPI検証】ファイルのアップロードはUploadFileよりStarletteのRequest.stream()のほうが速い」を起点にしました。同記事は約3GBを各3回送り、UploadFile平均25.8秒、Request.stream()平均12.8秒と報告しています。

一方、Redditの「I benchmarked 5 different FastAPI file upload methods」では、方式ごとにdisk書き込みの有無が違えば同じ仕事の比較にならない、という具体的な指摘がありました。

そこで今回は両方式とも、受信した全byteを同じdiskへ保存し、同じSHA-256を計算しました。multipartraw bodyというprotocol差は残りますが、片方だけ保存を省く比較にはしていません。

XServer VPSで使った実験環境は?

項目 実測条件
VPS XServer VPS、4 vCPU4GB RAM、150GB
OS Ubuntu 26.04
FastAPI 0.141.1
Starlette 1.6.0
container caseごとに新規作成、合計6個
上限 CPU 1RAM 384MiB、追加swapなし、PID 128
root filesystem read-only
test port 127.0.0.1:18025だけ
本番service CaddyFastAPIPostgreSQLを稼働したまま

外部から実験APIへ接続できないloopback bindにし、本番FastAPI containerは変更していません。test containercap-drop=ALLno-new-privilegesも設定しました。

[PR]
VPSでいろいろ試すなら『XServer VPS』

1・10・50MiBの送信結果は?

サイズ 方式 client中央値 cgroup peak peak増加 CPU 3回合計
1MiB UploadFile 20.809ms 39.13MiB 2.11MiB 0.0131秒
1MiB Request.stream 10.650ms 37.43MiB 0.10MiB 0.0079秒
10MiB UploadFile 41.491ms 59.55MiB 22.58MiB 0.1041秒
10MiB Request.stream 41.572ms 46.57MiB 9.62MiB 0.0508秒
50MiB UploadFile 153.864ms 139.93MiB 102.70MiB 0.4300秒
50MiB Request.stream 164.957ms 86.78MiB 49.44MiB 0.2262秒

peak増加は、API起動直後のbaselineから各case終了までのmemory.peak増分です。50MiBのRequest.stream()は、UploadFileより53.26MiB少ない増加で済みました。

Request.stream()はRAMをどれだけ減らした?

50MiB時のcgroup peakは53.15MiB減りました。baselineからの増加はUploadFile 102.70MiB、Request.stream() 49.44MiBで、streamは約半分です。

ただしPython process自体の最大resident memoryであるVmHWMは54.05MiB対50.63MiBで、差は約3.42MiBでした。cgroup値にはprocess RSSだけでなくfilepage cacheも含まれます。

UploadFilemultipartspooled temporary fileへ受け、その後endpointが保存先へ書きます。streamは受信chunkを保存先へ直接書きます。50MiBに近い差がcgroupで出たのは、このtemporary fileと保存先の二段階が主因だと考えられます。

Request.stream()はUploadFileより速かった?

今回の50MiBでは速くありませんでした。client end-to-end中央値はUploadFile 153.864ms、Request.stream() 164.957msで、UploadFileが11.093ms短い結果です。10MiBは41.491ms対41.572msで事実上同等でした。

Zennの約3GBではstreamが約2倍でしたが、今回の50MiBでは再現しませんでした。file sizestorageframework versionclienthost条件が違うため、「streamなら常に2倍速い」とは言えません。

一方、50MiBを3回処理したcontainer CPUは0.4300秒対0.2262秒で、Request.stream()が約47.4%少なくなりました。今回のVPSでは速度より、RAMCPUの節約が明確です。

実験中に何が失敗した?

初回はcontainer/tmpを96MiBのtmpfsにしました。1MiBと10MiBは通りましたが、UploadFileの50MiBでHTTP 500です。

UploadFileの約50MiB temporary fileと、endpointが作る約50MiBの保存fileが同じ96MiB tmpfsへ同時に存在する構造でした。容量を超えるうえ、tmpfsRAMなので「disk保存時のRAM」を測る条件としても不適切です。

対策は、randomhost一時directorycontainer/tmpbind mountし、disk-backed storageへ変えることでした。変更後は18/18回成功しました。

もう1つ、tmpfsを外すとDocker 29.7.2inspect結果でHostConfig.Tmpfs key自体が省略され、記録処理がKeyErrorになりました。未使用なら空objectとして扱うよう修正しました。設定値が空なのか、keyがないのかは別問題です。

本番のCaddyFastAPIPostgreSQLへ影響はあった?

public readyは実験前30.290ms、後27.295msで両方HTTP 200でした。status=readydatabase=okartifact_storage=okです。

本番containerの停止・再起動は0回。test container 6個、host一時directorytest fileは全削除しました。dangling volumeは前0件、後0件、swap freeも前後同じです。

root diskの前後差は69,632 byteでした。MemAvailableは約3268MiBから約3116MiBへ下がりましたが、containerfileは削除済みでswap増加はありません。Linuxpage cacheを本番host上で強制dropせず、そのまま記録しています。

AIスマホ調査の証拠アップロードはどちらを選ぶ?

VPSと自宅worker間の専用APIならRequest.stream()を第一候補にします。理由は50MiBでcgroup peakが約53MiB、CPUが約47%減ったためです。

ただしraw bodyだけでは元filenameartifact種別、端末ID、調査job IDなどをmultipart fieldとして一緒に送れません。headerまたは署名済みmetadataを別に設計します。

初心者が迷いそうなポイントは?

XServer VPSでこの悩みはどこまで解決した?

判定は一部解決です。4GB RAMでも50MiBを1件ずつ受ける実験は両方式で成功し、Request.stream()ならRAMCPUを抑えられました。一方、同時送信、TLS、外部回線、GB級動画、途中再開は未検証です。

「4GBだからuploadは無理」でも「streamなら必ず2倍速い」でもありません。今回得られた一次情報は、50MiB直列なら成功、速度は同等範囲、streamcgroup peak約53MiB減という限定付きの結論です。

よくある質問

Q. FastAPIのUploadFileは50MBをすべてRAMへ載せますか?

A. いいえ。UploadFileSpooledTemporaryFileを使い、一定量を超えるとdiskへ移ります。ただし今回のように別fileへ保存すると、一時fileと保存先の両方が同時に存在します。

Q. Request.stream()なら50MB以上でも安全ですか?

A. 自動的に安全にはなりません。size limit、認証、timeout、同時数、disk quota、途中file cleanupが必要です。

Q. 50MBのuploadXServer VPSは落ちましたか?

A. 落ちていません。公開readyは前後HTTP 200で、本番CaddyFastAPIPostgreSQLも継続稼働しました。

Q. Request.stream()は必ずUploadFileより速いですか?

A. 今回は違いました。50MiB client中央値は164.957ms対153.864msでstreamが少し遅く、速度優位は確認できませんでした。

Q. スクリーンショット収集にはどちらが向きますか?

A. browserから手軽に送るならUploadFile、自宅workerから専用APIへ大量送信するならRAMCPUを抑えたRequest.stream()が候補です。metadataの渡し方は別途設計します。

[PR]
XServer VPSの公式情報を確認する

実験日:2026年9月2日。数値は筆者が契約中のXServer VPS 4GB環境における単発の実測です。契約プラン、host負荷、software versionstoragenetworkにより結果は変わります。

コメント

タイトルとURLをコピーしました