Uploading Assets to fal Storage vs External CDN
fal.storage vs S3 vs Cloudflare R2. The cost and latency tradeoffs for image-to-video workflows.
The decision up front
For image to video on fal, you hand the model a URL. Three reasonable sources:
- fal.storage (
fal.storage.uploador Pythonupload_file) - your S3 bucket
- your Cloudflare R2 bucket
Not equivalent. The tradeoffs are real.
What fal.storage is doing
fal.storage.upload stores bytes in a fal managed bucket close to the inference infra, returns a fal.media URL. Two properties follow.
- First byte latency from the GPU to your image is low, often noticeably lower than S3 in a different region.
- The URL persists long enough for retries, but it is not your asset.
Use it when speed matters and the image is ephemeral.
What S3 and R2 give you
- Lifecycle and retention policy you control
- Cost you can audit
- An address you keep regardless of what fal does
S3 storage is cheap; S3 egress is not. R2 storage is similar; R2 egress is free if you deliver through Cloudflare. For tens of thousands of videos per day, R2 wins on blended cost because you stop paying egress.

Latency, honestly
A rough production number: fal.storage upload to first inference byte is typically 200 to 400 ms faster than an S3 URL in a different region, on a 2 MB reference. Gap shrinks under 100 ms when S3 is co-located with transfer acceleration. Grows to 800 ms or more across oceans.
For a single render, irrelevant. For a studio where users iterate 30 times on one prompt, 400 ms per call is 12 seconds of spinner time.
The two call patterns

Bytes to fal.storage (fastest first render):
1import { fal } from "@fal-ai/client";23const fileUrl = await fal.storage.upload(file);4const result = await fal.subscribe("fal-ai/wan/v2.7/image-to-video", {5 input: { prompt: "gentle pan", image_url: fileUrl, duration: 5 },6});
URL from your own bucket (keeps the asset):
1const result = await fal.subscribe("fal-ai/wan/v2.7/image-to-video", {2 input: { prompt: "gentle pan", image_url: "https://cdn.yourapp.com/ref.png", duration: 5 },3});
The pattern that wins
For user uploads, do both. Upload to fal.storage for the first render (fast iteration). Mirror to your R2 bucket in background. If the user tweaks, the fal URL is still warm. If they return tomorrow, the R2 URL is still yours.
1const [falUrl] = await Promise.all([2 fal.storage.upload(file),3 r2.putObject({ Bucket, Key, Body: file }),4]);56await fal.subscribe("fal-ai/wan/v2.7/image-to-video", {7 input: { prompt, image_url: falUrl, duration: 5 },8});
Cost model with real per second prices
1000 image-to-video renders per day, 2 MB reference each.
- fal.storage only: no storage cost, no egress, the reference is not yours.
- R2: 2 GB/day storage, pennies. Egress to fal, free. Delivery of the final video via Cloudflare, free. You pay for storage, not movement.
- S3 us-east-1: same storage, plus generation pulls and end user egress at roughly $0.08/GB. At 50 MB average final video, 1000 plays/day, that is about $4/day or $120/month in egress.
Generation cost: Wan 2.7 at $0.10/sec, 5 sec = $0.50, 1000 clips = $500/day. The input is not the expensive part. Final delivery is.
When to stay on fal.storage forever
If users stream once and never revisit, fal.storage is fine end to end. The moment a user comes back next week and expects that video, or you want your own analytics, you own the asset. Mirror it.
The anti-pattern
Do not upload user bytes to your bucket first, then presign a URL, then pass it to fal. You just added a round trip and three failure points. Upload to fal.storage for speed; mirror to your bucket in parallel for retention.