fal
The wire to fal.ai's fast, pre-warmed media endpoints: call a model by id, control the queue, get the file back. fal is the fast-media path — latency-optimized image (FLUX, Seedream, SD), audio (TTS, music), and video (Veo, Wan, Kling, Hailuo) endpoints you invoke by id with FAL_KEY.
You own the mechanics: auth, call mode, queue states, webhook signatures, file I/O, per-call cost.
When NOT to use
| You want… | Go to |
|---|---|
| Which model / what to generate / art direction / multi-provider media pipeline | ai-media |
The same kind of models on Replicate (replicate.run / predictions) |
replicate — images-specifically replicate-images |
| Renting a raw GPU pod you SSH into to train or custom-serve | runpod |
| Deploying your own Python function as an autoscaling endpoint | modal |
| Cheap hosted LLM text/chat completions | together-fireworks |
| The generic provider-agnostic webhook receiver/verifier pattern | webhooks |
Rule: if you are not invoking a fal endpoint id with FAL_KEY, you are in the wrong skill.
Setup & auth
# JS — current client. NOT @fal-ai/serverless-client (deprecated, migrate).
npm i @fal-ai/client # latest 1.10.1
# Python
pip install fal-client
export FAL_KEY="key_id:key_secret"
import { fal } from "@fal-ai/client";
// Reads FAL_KEY from env automatically; or set it explicitly:
fal.config({ credentials: process.env.FAL_KEY });
Rule: never ship FAL_KEY to a browser bundle. Proxy every call through your own server. Why: a key in client-side JS lets anyone drain your account — fal endpoints bill per call with no per-request cap.
Pick a call mode
All three modes hit the same queue. Choose by how long the job runs and where you call it from.
| Situation | Mode | Why |
|---|---|---|
| Need the result now, can block, single short job (image, short TTS) | subscribe |
Submits + auto-polls until done; feels synchronous, no polling code |
| Long job (video), batch, or running in a serverless/edge handler that can't hold a connection | submit + webhook_url (or poll) |
Returns a request_id instantly; result arrives later, no held connection |
| Trivially short call, you accept no queue control | run |
Direct synchronous call — no status, no logs; drops on long jobs |
// Bad: run() on a 60s video — connection can drop, no retry, no progress.
const res = await fal.run("fal-ai/veo3", { input });
// Good: submit + webhook for anything that takes more than a few seconds.
const { request_id } = await fal.queue.submit("fal-ai/veo3", {
input,
webhookUrl: "https://api.example.com/fal/webhook",
});
subscribe — block and stream progress
const result = await fal.subscribe("fal-ai/flux/dev", {
input: { prompt: "a red bicycle on a wet street, cinematic" },
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
update.logs?.forEach((l) => console.log(l.message)); // stream to user
}
},
});
console.log(result.data.images[0].url); // hosted output URL
import fal_client
def on_update(update):
if isinstance(update, fal_client.InProgress):
for log in update.logs:
print(log["message"])
result = fal_client.subscribe(
"fal-ai/flux/dev",
arguments={"prompt": "a red bicycle on a wet street, cinematic"},
with_logs=True,
)
print(result["images"][0]["url"])
Python has an async twin for every method — subscribe_async, submit_async, run_async. Use them inside an event loop.
submit + queue polling
When you cannot or will not block, submit and poll the queue yourself.
const { request_id } = await fal.queue.submit("fal-ai/flux/dev", { input });
// Poll. Status moves IN_QUEUE -> IN_PROGRESS -> COMPLETED.
const status = await fal.queue.status("fal-ai/flux/dev", {
requestId: request_id,
logs: true,
});
// Once COMPLETED, fetch the result.
const result = await fal.queue.result("fal-ai/flux/dev", { requestId: request_id });
console.log(result.data.images[0].url);
Rule: back off between polls — start at ~1s, grow to a few seconds. Why: a tight while loop polling queue.status hammers the API and gains nothing; the job finishes when it finishes. For anything long-running, prefer a webhook over any polling at all.
Webhooks
Pass webhook_url (camelCase webhookUrl in the JS client) on submit; fal POSTs the result when the job finishes.
// Success
{ "request_id": "...", "gateway_request_id": "...", "status": "OK", "payload": { /* result */ } }
// Failure
{ "request_id": "...", "status": "ERROR", "error": "..." }
// Result couldn't be serialized
{ "request_id": "...", "status": "OK", "payload": null, "payload_error": "..." }
Delivery facts you must design for:
- The initial POST has a 15-second timeout. On timeout or non-2xx, fal retries up to 10 times over ~2 hours.
- Therefore your handler must be idempotent — the same
request_idcan arrive more than once. Dedupe onrequest_id(e.g. an upsert keyed on it) before acting. - Verify the ED25519 signature before trusting the body — four
X-Fal-Webhook-*headers + a JWKS fetched fromhttps://rest.fal.ai/.well-known/jwks.json. Why: an unverified webhook endpoint is a public write to your DB / spend trigger.
The full verification (header parsing, JWKS caching, ±5-minute timestamp check, message construction, per-key verify) and a complete idempotent handler in Node and Python live in references/queue-and-webhooks.md.
File inputs and outputs
Upload a local file to get a URL, then pass that URL into input for image-to-X jobs. Outputs always come back as hosted URLs.
const url = await fal.storage.upload(file); // File/Blob -> hosted URL
const out = await fal.subscribe("fal-ai/flux/dev/image-to-image", {
input: { image_url: url, prompt: "make it snow" },
});
url = fal_client.upload_file("input.png")
out = fal_client.subscribe(
"fal-ai/flux/dev/image-to-image",
arguments={"image_url": url, "prompt": "make it snow"},
)
Cost control
Pricing is pay-per-use, per-model unit — never flat. The unit differs by model, so always read the model's pricing tab before you ship a loop.
| Unit | Used by | 2026 example |
|---|---|---|
| Per image | image diffusion | Seedream V4 ~`$0.03`/image |
| Per second of output | video | Wan 2.5 ~`$0.05/s; Veo 3 ~$0.4`/s |
| Per megapixel | some image models | varies — read the tab |
| GPU-hour | fal-served compute | A100 40GB $0.99/h, H100 80GB $1.89/h (2026-05-13) |
Spend knobs, by modality:
- Image: lower
num_inference_steps, drop resolution / megapixels, cutnum_images. - Video: shorten
duration, lower fps/resolution — per-second pricing scales linearly. - Batch: fal batch inference is 50% of serverless price — use it for offline bulk jobs where latency does not matter.
Worked estimate: 500 Seedream V4 images at ~$0.03 ≈ $15 serverless, ≈ $7.50 on the batch path.
The full model-family map and per-modality knob list live in references/models-and-cost.md.
Anti-patterns
| Anti-pattern | Why it bites | Do instead |
|---|---|---|
FAL_KEY in a browser bundle |
Anyone reads it and bills your account | Proxy through your server |
run for a 30–60s video |
Connection drops, no retry, no progress | submit + webhook_url |
| Webhook handler with no signature check | Public write / spend trigger anyone can forge | Verify ED25519 against the JWKS |
| Non-idempotent webhook handler | 10 retries over 2h create duplicate side effects | Dedupe on request_id |
| Ignoring the model's pricing unit | "$0.05" is per-second, not per-video — surprise bill | Read the pricing tab; pick the right knob |
Tight while loop on queue.status |
Hammers the API, gains nothing | Back off, or use a webhook |
@fal-ai/serverless-client |
Deprecated; missing fixes and APIs | @fal-ai/client (v1.10.1) |