【PR】この記事にはA8.netのアフィリエイト広告を含みます。
先に結論:4GBのXServer VPSへ50MiBの証拠ファイルを送る実験は、FastAPIのUploadFileとRequest.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へ送れる?
- UploadFileとRequest.stream()は何が違う?
- なぜ同じdisk保存・SHA-256までそろえた?
- XServer VPSで使った実験環境は?
- 1・10・50MiBの送信結果は?
- Request.stream()はRAMをどれだけ減らした?
- Request.stream()はUploadFileより速かった?
- 実験中に何が失敗した?
- 本番のCaddy・FastAPI・PostgreSQLへ影響はあった?
- AIスマホ調査の証拠アップロードはどちらを選ぶ?
- 初心者が迷いそうなポイントは?
- XServer VPSでこの悩みはどこまで解決した?
- よくある質問
- 最新条件の確認先
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へ書く | 受信chunkをdiskへ書く |
| 今回の整合性 | 保存中にSHA-256 | 保存中にSHA-256 |
FastAPI公式のRequest Filesでは、UploadFileは一定量までmemory、その後diskへ移るspooled fileを使うと説明されています。Starlette公式のRequest bodyでは、request.stream()はbody全体をmemoryへ保存せずchunkを受け取れると説明されています。
なぜ同じdisk保存・SHA-256までそろえた?
元の悩みは、ZennのNishika 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を計算しました。multipartとraw bodyというprotocol差は残りますが、片方だけ保存を省く比較にはしていません。
XServer VPSで使った実験環境は?
| 項目 | 実測条件 |
|---|---|
| VPS | XServer VPS、4 vCPU、4GB RAM、150GB |
| OS | Ubuntu 26.04 |
| FastAPI | 0.141.1 |
| Starlette | 1.6.0 |
| container | caseごとに新規作成、合計6個 |
| 上限 | CPU 1、RAM 384MiB、追加swapなし、PID 128 |
| root filesystem | read-only |
| test port | 127.0.0.1:18025だけ |
| 本番service | Caddy、FastAPI、PostgreSQLを稼働したまま |
外部から実験APIへ接続できないloopback bindにし、本番FastAPI containerは変更していません。test containerはcap-drop=ALLとno-new-privilegesも設定しました。
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だけでなくfileのpage cacheも含まれます。
UploadFileはmultipartをspooled 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 size、storage、framework version、client、host条件が違うため、「streamなら常に2倍速い」とは言えません。
一方、50MiBを3回処理したcontainer CPUは0.4300秒対0.2262秒で、Request.stream()が約47.4%少なくなりました。今回のVPSでは速度より、RAMとCPUの節約が明確です。
実験中に何が失敗した?
初回はcontainerの/tmpを96MiBのtmpfsにしました。1MiBと10MiBは通りましたが、UploadFileの50MiBでHTTP 500です。
UploadFileの約50MiB temporary fileと、endpointが作る約50MiBの保存fileが同じ96MiB tmpfsへ同時に存在する構造でした。容量を超えるうえ、tmpfsはRAMなので「disk保存時のRAM」を測る条件としても不適切です。
対策は、randomなhost一時directoryをcontainerの/tmpへbind mountし、disk-backed storageへ変えることでした。変更後は18/18回成功しました。
もう1つ、tmpfsを外すとDocker 29.7.2のinspect結果でHostConfig.Tmpfs key自体が省略され、記録処理がKeyErrorになりました。未使用なら空objectとして扱うよう修正しました。設定値が空なのか、keyがないのかは別問題です。
本番のCaddy・FastAPI・PostgreSQLへ影響はあった?
public readyは実験前30.290ms、後27.295msで両方HTTP 200でした。status=ready、database=ok、artifact_storage=okです。
本番containerの停止・再起動は0回。test container 6個、host一時directory、test fileは全削除しました。dangling volumeは前0件、後0件、swap freeも前後同じです。
root diskの前後差は69,632 byteでした。MemAvailableは約3268MiBから約3116MiBへ下がりましたが、containerとfileは削除済みでswap増加はありません。Linuxのpage cacheを本番host上で強制dropせず、そのまま記録しています。
AIスマホ調査の証拠アップロードはどちらを選ぶ?
VPSと自宅worker間の専用APIならRequest.stream()を第一候補にします。理由は50MiBでcgroup peakが約53MiB、CPUが約47%減ったためです。
ただしraw bodyだけでは元filename、artifact種別、端末ID、調査job IDなどをmultipart fieldとして一緒に送れません。headerまたは署名済みmetadataを別に設計します。
- body受信前にBearer tokenを検証する。
- Content-Lengthと実受信byteの両方に上限を置く。
- 受信中にSHA-256を計算し、client申告hashと比較する。
- 一時名へ保存し、hash一致後だけatomic renameする。
- jobごとのquotaと同時upload数を制限する。
- 途中切断時は一時fileを削除する。
- 大きな動画はobject storageのpresigned URLも比較する。
初心者が迷いそうなポイントは?
UploadFileはfile全体をPython bytesへ載せる方式ではないが、temporary fileは使う。request.stream()を使っても、chunkを結合すれば結局RAMを消費する。- tmpfsはdisk風のpathでもRAMを使う。
- process RSSとcontainer cgroup memoryは同じ数値ではない。
- endpoint内部timerだけではmultipart parse前の時間を含まない。
- localhostの速さをインターネット越しへそのまま当てはめない。
- raw bodyではfilenameなどのmetadataを別送する必要がある。
XServer VPSでこの悩みはどこまで解決した?
判定は一部解決です。4GB RAMでも50MiBを1件ずつ受ける実験は両方式で成功し、Request.stream()ならRAMとCPUを抑えられました。一方、同時送信、TLS、外部回線、GB級動画、途中再開は未検証です。
「4GBだからuploadは無理」でも「streamなら必ず2倍速い」でもありません。今回得られた一次情報は、50MiB直列なら成功、速度は同等範囲、streamはcgroup peak約53MiB減という限定付きの結論です。
よくある質問
Q. FastAPIのUploadFileは50MBをすべてRAMへ載せますか?
A. いいえ。UploadFileはSpooledTemporaryFileを使い、一定量を超えるとdiskへ移ります。ただし今回のように別fileへ保存すると、一時fileと保存先の両方が同時に存在します。
Q. Request.stream()なら50MB以上でも安全ですか?
A. 自動的に安全にはなりません。size limit、認証、timeout、同時数、disk quota、途中file cleanupが必要です。
Q. 50MBのuploadでXServer VPSは落ちましたか?
A. 落ちていません。公開readyは前後HTTP 200で、本番Caddy、FastAPI、PostgreSQLも継続稼働しました。
Q. Request.stream()は必ずUploadFileより速いですか?
A. 今回は違いました。50MiB client中央値は164.957ms対153.864msでstreamが少し遅く、速度優位は確認できませんでした。
Q. スクリーンショット収集にはどちらが向きますか?
A. browserから手軽に送るならUploadFile、自宅workerから専用APIへ大量送信するならRAM・CPUを抑えたRequest.stream()が候補です。metadataの渡し方は別途設計します。
実験日:2026年9月2日。数値は筆者が契約中のXServer VPS 4GB環境における単発の実測です。契約プラン、host負荷、software version、storage、networkにより結果は変わります。

コメント