fal, the queue and the cost of generating media
fal is a closed platform that exposes ready made image, video and audio generation models through an API, and on top of that lets you deploy your own code on its GPUs. Whether it fits a given project comes down to two things: how it queues long running requests, and which unit you are actually paying for.
What fal does and does not do
The basic scenario is simple. You pick an endpoint identifier from the gallery, for example fal-ai/flux/schnell, send JSON with the input parameters and get back URLs of the generated files. You do not manage GPUs, you do not build a container image, you do not watch scaling. The model weights are already loaded on the other side, and billing follows the output rather than the clock.
The second scenario is fal Serverless. You deploy your own Python application, get your own endpoint and pay for the lifetime of the runner process. That is closer to what Modal does, only with a less thoroughly documented programming model and with one significant catch in billing, covered below.
What fal does not do. There is no self hosted variant. The platform code is not open: only the client libraries are. There is also no guarantee that a specific model version from the gallery will still be there in a year, because some entries are partner models hosted by third parties whose availability is managed by the partner rather than by fal. If your architecture needs the option of moving the workload onto your own hardware, look towards vLLM or weights pulled from Hugging Face instead.
There are two clients: @fal-ai/client for JavaScript and TypeScript, and fal-client for Python. Both talk to the same HTTP API, so if there is no client for your language, plain curl is entirely sufficient.
Client versions and the licence from three sources
Checking the licence in three places at once produces a result here that the registry metadata alone does not show.
The JavaScript client is fine. Package @fal-ai/client version 1.10.1 was published on 4 May 2026. The license field in the npm registry says MIT. The unpacked archive contains a LICENSE file with the MIT text and the copyright notice Copyright 2024 https://fal.ai. Repository fal-ai/fal-js carries the same file on the main branch. Three sources, one answer.
The Python client looks different. Package fal-client 1.0.1 was released on 19 August 2026. In the PyPI metadata the license field is empty, there is no license_expression field at all, and not a single classifier starts with License ::. A licence scanner querying PyPI alone will see a package with no licence. The wheel contents say otherwise: the file fal_client-1.0.1.dist-info/licenses/LICENSE exists and holds the full text of the Apache License 2.0, and the License-File: LICENSE header in METADATA confirms the build tool did notice it.
The cause is visible in the configuration. In projects/fal_client/pyproject.toml the [project] section declares name, dynamic, description, readme, authors, requires-python and dependencies, but there is no license key and no licence classifier either. The file projects/fal_client/LICENSE is itself a link to ../../LICENSE, that is to the licence of the whole fal-ai/fal repository, which is Apache 2.0. Setuptools shipped the file inside the wheel but had nowhere to take an identifier from for the metadata.
The practical conclusion: the JavaScript client is MIT, the Python client is Apache 2.0. Both are permissive, but they are not the same licence, and Apache 2.0 adds a patent clause and a requirement to mark modifications. If your compliance process reads registry metadata rather than package contents, you will get an alert about a package with no licence and will have to clear it by hand.
Both packages contain real code, which after the history of empty stubs on npm is not a given. The npm archive holds roughly 95 kilobytes of compiled JavaScript in the src directory along with type declarations. The Python wheel holds a client.py module over 2600 lines long plus auth.py, _headers.py and py.typed.
The differing release rhythm does not mean stagnation. The JavaScript client has been sitting at 1.10.1 since May because it lives in its own fal-ai/fal-js repository, where the last release was tagged client-v1.10.1 on 4 May 2026. The Python client lives in the fal-ai/fal monorepo alongside the rest of the platform tooling, and traffic there is far heavier: fal_v1.79.1 on 5 August, isolate_proto_v0.34.2 on 19 August, fal_client_v1.0.1 on the same day. Two different repositories on two different cycles, not an abandoned project.
The third package in that family, @fal-ai/server-proxy version 1.2.1 from 20 February 2026, is older still. It is MIT licensed and declares only optional peer dependencies on frameworks: next in the range 13.4 - 14 || >=15.0.0-0, react in ^18.0.0 || >=19.0.0-0, hono in ^4.0.0, express in ^4.0.0, @sveltejs/kit in ^2.0.0 and @remix-run/dev in ^2.0.0. It declares no peer dependency on @fal-ai/client at all, so installing both at once will not produce a version conflict, but nobody promises that a proxy from February understands every change in a client from May.
The queue, the mode you have to know
This is the heart of the platform. Video generation takes minutes, so a synchronous call holding an HTTP connection open for that long is fragile, and behind a serverless gateway or on an edge function it will simply die on the time limit. fal solves this with a separate, durable queue at https://queue.fal.run, as opposed to https://fal.run used by direct calls.
A queued request moves through three states:
| Status | Python SDK type | JavaScript SDK type | What is happening |
|---|---|---|---|
IN_QUEUE | Queued(position) | "IN_QUEUE" with a queue_position field | Request accepted, waiting for a free runner |
IN_PROGRESS | InProgress(logs) | "IN_PROGRESS" with a logs field | A runner picked the request up and is computing |
COMPLETED | Completed(logs, metrics) | "COMPLETED" with logs and metrics fields | Result ready to fetch or delivered by webhook |
Installing both clients looks like this:
# JavaScript client, version pinned exactly
pnpm add @fal-ai/client@1.10.1
# Python client
pip install "fal-client==1.0.1"
# the key is read from this environment variable by default
export FAL_KEY="your-key"The simplest way into the queue is subscribe. The method sends the request to the queue, polls for status itself and returns the result once it is ready. The interface is blocking, the mechanism underneath is the queue:
import { fal } from "@fal-ai/client";
const result = await fal.subscribe("fal-ai/flux/schnell", {
input: { prompt: "a sunset over mountains" },
logs: true,
mode: "polling",
pollInterval: 1000,
priority: "normal",
onEnqueue: (requestId) => console.log("queued:", requestId),
onQueueUpdate: (status) => {
if (status.status === "IN_QUEUE") {
console.log("position:", status.queue_position);
} else if (status.status === "IN_PROGRESS") {
status.logs?.forEach((log) => console.log(log.message));
}
},
});
console.log(result.data.images[0].url, result.requestId);The second mode separates sending from receiving. fal.queue.submit returns immediately, and you collect the result later, from a different process, using the stored request_id:
import { fal } from "@fal-ai/client";
// 1. submit, returns immediately
const enqueued = await fal.queue.submit("fal-ai/flux/dev", {
input: { prompt: "a sunset over mountains" },
webhookUrl: "https://example.com/api/fal/webhook",
priority: "low",
startTimeout: 120,
});
console.log(enqueued.request_id, enqueued.status_url, enqueued.cancel_url);
// 2. check status, at any later point
const status = await fal.queue.status("fal-ai/flux/dev", {
requestId: enqueued.request_id,
logs: true,
});
// 3. fetch the result once the status is COMPLETED
if (status.status === "COMPLETED") {
const result = await fal.queue.result("fal-ai/flux/dev", {
requestId: enqueued.request_id,
});
console.log(result.data);
}
// 4. cancel when the job is no longer needed
await fal.queue.cancel("fal-ai/flux/dev", { requestId: enqueued.request_id });Beyond those four methods the JavaScript client also has fal.queue.streamStatus, which opens a streaming connection instead of polling, and fal.queue.subscribeToStatus, that is waiting for an already submitted request to finish.
The Python side names the same things differently. Instead of a queue object you get a handle returned by submit:
import fal_client
# submit returns a SyncRequestHandle, not the result
handler = fal_client.submit(
"fal-ai/flux/dev",
arguments={"prompt": "a sunset over mountains"},
webhook_url="https://example.com/api/fal/webhook",
priority="low",
start_timeout=120,
)
print(handler.request_id, handler.status_url, handler.cancel_url)
# a single status check
status = handler.status(with_logs=True)
if isinstance(status, fal_client.Queued):
print("position:", status.position)
elif isinstance(status, fal_client.InProgress):
for log in status.logs or []:
print(log["message"])
# stream events until completion, then take the result
for event in handler.iter_events(with_logs=True):
if isinstance(event, fal_client.InProgress):
pass
result = handler.get(interval=0.5)
print(result["images"][0]["url"])
# blocking variant with a client side deadline of your own
result = fal_client.subscribe(
"fal-ai/flux/schnell",
arguments={"prompt": "a sunset over mountains"},
with_logs=True,
interval=0.5,
client_timeout=600,
)The SyncRequestHandle has the fields request_id, response_url, status_url and cancel_url plus a from_request_id class method that lets you rebuild it in another process from the identifier alone. The asynchronous variant is called AsyncRequestHandle and is returned by the functions carrying the _async suffix: submit_async, subscribe_async, run_async, status_async, result_async, cancel_async.
Without any client at all it looks like this:
# submit to the queue, webhook passed as a query parameter
curl -X POST "https://queue.fal.run/fal-ai/flux/dev?fal_webhook=https://example.com/api/fal/webhook" \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-H 'X-Fal-Object-Lifecycle-Preference: {"expiration_duration_seconds": 3600}' \
-d '{"prompt": "a sunset over mountains"}'
# status including runner logs
curl "https://queue.fal.run/fal-ai/flux/dev/requests/$REQUEST_ID/status?logs=1" \
-H "Authorization: Key $FAL_KEY"
# fetch the result
curl "https://queue.fal.run/fal-ai/flux/dev/requests/$REQUEST_ID/response" \
-H "Authorization: Key $FAL_KEY"The status response has three different shapes depending on the state. The one after completion carries a measurement of the compute time:
{
"status": "COMPLETED",
"request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
"response_url": "https://queue.fal.run/fal-ai/flux/dev/requests/764cabcf.../response",
"logs": [{ "message": "Done.", "timestamp": "2026-02-17T10:30:05.789Z" }],
"metrics": { "inference_time": 3.42 }
}The metrics.inference_time field appears only with status COMPLETED and reports the seconds spent computing. The error and error_type fields appear there only when the request failed.
Direct mode and subscribe mode
The run method is the only call that never touches the queue. It goes straight to fal.run, the response comes back on the same HTTP connection, and there is neither a queue position nor server side retries. The client retries transient 502, 503 and 504 errors on its own, but when the connection drops, the request goes with it.
The difference in time limits is asymmetric between the clients and it is a real trap. In Python, run accepts timeout as a client side HTTP limit and start_timeout as a server side deadline for processing to begin. In JavaScript, fal.run() supports neither timeout nor hint nor headers, only startTimeout. To set headers, a runner hint or a priority from JavaScript you have to move to fal.queue.submit or fal.subscribe.
The start_timeout parameter in both clients measures only the time until a runner starts computing, including queue wait, retries and routing. Once processing begins it no longer applies. Exceeding it yields a 504 and is the only situation in which a request waiting in the queue can be dropped. The header carrying it is called x-fal-request-timeout.
The default polling rate differs fivefold between the clients, and this does not come from the documentation but from the package code. In the JavaScript client the DEFAULT_POLL_INTERVAL constant is 500 milliseconds. In the Python client DEFAULT_QUEUE_POLL_INTERVAL is 0.1 seconds, that is 100 milliseconds. With hundreds of parallel requests from Python that is five times more status calls than from JavaScript, so set interval explicitly.
The guidance on what to choose is short. run suits scripts and prototypes where you want the shortest path. subscribe gives the same blocking interface but with queue reliability, and it is the one the vendor recommends as the default. submit with a webhook is the only sensible choice for video, training and anything that takes minutes.
Concurrency limits, storage and webhooks
A new account starts with a limit of two simultaneous requests in the IN_PROGRESS state. The limit grows automatically based on the total of paid invoices from the last four weeks and, in self serve mode, reaches forty. Above forty the conversation moves to the sales team. Requests in the IN_QUEUE state do not count against the limit, and the limit itself cannot be exceeded in a way that gets a request rejected: for queued calls the platform simply retries the dispatch with growing backoff and with no maximum number of attempts.
For direct calls the answer to exceeding the limit is a 429 with type concurrent_requests_limit and the header X-Fal-needs-retry: 1. The client retries by itself, up to ten times with growing backoff. If you write raw HTTP requests, that header has to be handled by hand.
The queue also retries after runner failures. A 503, a 504 or a dropped connection mid computation causes the request to be requeued, up to ten times. There is no queue size limit.
The webhook receives a POST once processing finishes. The body contains request_id, gateway_request_id, a status of either OK or ERROR, and a payload with the result. Those two identifiers are usually the same, but when a request was retried, gateway_request_id points at the last attempt while request_id stays the one from the queue. Keeping them apart matters when matching a webhook to a database record.
Output files land on the CDN under addresses like https://v3b.fal.media/files/... and are public by default: anyone holding the address holds the file. Changing that requires the X-Fal-Object-Lifecycle-Preference header, in which besides expiration_duration_seconds you can pass initial_acl. In the Python client this corresponds to the StorageSettings type with the fields expires_in and initial_acl, ObjectExpiration accepting the values never, immediate, 1h, 1d, 7d, 30d, 1y or a number of seconds, and StorageACLRule with a decision of hide, forbid or allow.
Here the documentation contradicts itself. The FAQ says output files are available for at least seven days by default. The summary table on the data retention page describes retention for generated media as configurable, with no number at all. For JSON payloads both pages agree on thirty days with an opt out through the X-Fal-Store-IO: 0 header. When planning, take seven days as the lower bound and download the files to your own storage anyway.
Pricing worked through examples
The fal.ai/pricing page renders without JavaScript, so the figures below come from the raw HTML fetched on 22 August 2026. The pricing has two entirely separate parts, and confusing them is the most common mistake in cost estimation.
The first part covers your own deployments and counts GPU hours:
| GPU | VRAM | List price | Lowest price |
|---|---|---|---|
| B300 | 288 GB | 8.50 USD/h | 4.49 USD/h |
| B200 | 180 GB | 6.25 USD/h | 3.49 USD/h |
| H200 | 141 GB | 4.50 USD/h | 2.10 USD/h |
| H100 | 80 GB | 4.50 USD/h | 1.89 USD/h |
| RTX PRO 6000 | 96 GB | 2.99 USD/h | 1.10 USD/h |
In that table the H100 and the H200 carry an identical list price of 4.50 USD per hour despite a 61 GB difference in memory, and they only diverge in the lowest price column. The vendor does not explain what it takes to reach that second column, beyond noting that it applies to custom deployments arranged by contacting support. Treat the list column as the one you will actually pay.
The second part covers ready made models from the gallery and counts output units:
| Model | Unit | Price | Thousand images or one minute of video |
|---|---|---|---|
| Seedream V4 | image | 0.03 USD | 30.00 USD per thousand images |
| Flux Kontext Pro | image | 0.04 USD | 40.00 USD per thousand images |
| Nanobanana | image | 0.0398 USD | 39.80 USD per thousand images |
| Qwen | megapixel | 0.02 USD | 20.00 USD per thousand 1 MP images |
| Wan 2.5 | video second | 0.05 USD | 3.00 USD per minute |
| Kling 2.5 Turbo Pro | video second | 0.07 USD | 4.20 USD per minute |
| Veo 3 | video second | 0.40 USD | 24.00 USD per minute |
| Ovi | video | 0.20 USD | depends on the length of a single clip |
The last column is my own arithmetic, the vendor publishes unit prices only. With Qwen billed per megapixel a 2 MP image costs twice as much, because the documentation states the proportional adjustment for higher resolutions explicitly. With Ovi no price per minute can be given, because the unit is a whole clip rather than a second: taking the vendor's own footnote assumption of an average five second clip, a minute would need twelve clips, that is 2.40 USD, but this is an assumption and not a price from the price list.
The pricing arithmetic itself has one flaw. The "Output per $1" column gives fourteen seconds for Kling 2.5 Turbo Pro, a correct rounding down from 14.28. For Veo 3 it gives three seconds, whereas one dollar divided by 0.40 USD is exactly 2.5 seconds. One rounded down, the other rounded up, and the gap is twenty percent against the reader. The unit prices themselves are consistent, so calculate from those and not from the comparison column.
Putting both parts together produces an interesting result. A thousand images from Seedream V4 costs 30 USD. That same thousand images computed on your own deployment on an H100, assuming two seconds per image, is 2000 seconds at 0.00125 USD, that is 2.50 USD. The difference is twelvefold, but the comparison is unfair on three counts: you need the weights and the right to use them, you need to optimise them down to two seconds, and you also pay for the states in which the GPU computes nothing.
That third point is the crucial one for Serverless. The billing documentation lists the runner states and explicitly marks as billable: SETUP, meaning execution of your setup() method with model loading, IDLE, meaning readiness including the keep_alive window, RUNNING, DRAINING and TERMINATING. Only PENDING, DOCKER_PULL and TERMINATED are free. Sixty seconds of idling on an H100 at the list price is 0.075 USD for every scale down. Multi GPU machines are counted as gpu_count times duration.
With ready made gallery models the situation is reversed, and this is a real advantage: cold starts are not billed, because you pay for the output rather than for seconds. Time spent waiting in the queue is not billed either.
There is no free plan in the usual sense. Billing is prepaid: you buy credits and they are drawn down. Purchased credits expire 365 days after the purchase. Free credits and coupons do exist, but the vendor does not state their size and describes their validity as variable, from a week to a year depending on the particular grant. When the balance falls below the lock threshold, the account is locked and requests are rejected; the value of that threshold is not documented. Accounts on invoice billing are not subject to automatic locking.
The billing documentation contradicts itself on one point. The model pricing page says you pay only for successful outputs and are never charged for server errors. The FAQ says the same about errors of 500 and above, but adds that client side errors, for example a 422 on invalid input, may still be charged if a runner consumed GPU time before the error was detected. Those are two different answers to the same question, and for budgeting purposes stick with the more cautious one.
fal, Replicate and Modal
The three platforms solve overlapping problems, but you enter them from different directions.
| Dimension | fal | Replicate | Modal |
|---|---|---|---|
| Entry point | a model id from the gallery | a model id from the gallery | your own code with decorators |
| Bringing your own code | fal Serverless, Python | Cog building a container image | the modal package, Python |
| Billing for gallery models | per output unit, falling back to GPU seconds | per output unit or per second, depending on the model | no gallery |
| Billing for your own code | per runner lifetime, IDLE billable | per second of hardware time | per second, separately for CPU, memory and GPU |
| Mode for long jobs | the queue.fal.run queue with webhooks | webhook or prediction polling | background calls and queues in code |
| Client licence | MIT on npm, Apache 2.0 on PyPI | Apache 2.0 for Cog | Apache 2.0 |
Against Replicate the difference is subtler than it looks. There is a gallery there too and two billing models too, but the choice between them depends on how a given model was published, so for one model you pay per image and for the next per GPU second. fal sets the rule the other way round: per output unit billing is the default, and falling back to GPU seconds is the exception for models without a fixed output price and for your own endpoints. In practice this means a bill is easier to estimate up front on fal. On the Replicate side, though, the Python client library has had no stable release since May 2025, whereas fal-client shipped three days ago.
Modal is a different league and comparing them head on misses the point. There is no gallery of ready made models there: you write a Python function, decorate it and deploy it, and billing runs purely on seconds, separately for cores, memory and GPU. If your job is generating a thousand images with a popular model, Modal requires you to build the whole path that fal already has. If your job is a bespoke processing pipeline with unusual dependencies, fal Serverless will be the tighter corset. One aside worth noting: both platforms bill idle time, only fal admits it outright in its state table, while Modal promises the opposite in its pricing headline to what it writes in its FAQ.
For language models this trio is not the first choice. There it is simpler to reach for the OpenAI API or stand up vLLM on your own hardware. If, on the other hand, you need to run model generated code in isolation rather than compute weights on a GPU, the right tool is E2B.
Common mistakes
Calling run for video generation. That mode holds an HTTP connection open for the whole computation and has no queue behind it, so the first gateway or edge function with a thirty second limit will cut the request off and the result is gone with no way to recover it. For anything longer than a few seconds use submit with a webhook, or at least subscribe.
Mixing naming conventions in responses. The result returned by run, subscribe and fal.queue.result in the JavaScript client has the shape { data, requestId } with a camel case identifier, while the status objects from fal.queue.status carry request_id with an underscore, because that is raw JSON from the API. Code that passes one where the other is expected gets undefined and raises nothing.
Leaving the default polling in Python. A hundred parallel requests at a DEFAULT_QUEUE_POLL_INTERVAL of 0.1 seconds means a thousand status calls per second purely on waiting. Set interval to something on the order of a second or move to webhooks.
Assuming the key can sit in the browser. The client detects a browser environment and warns, and switching the warning off with suppressLocalCredentialsWarning does not change the fact that the key is then visible. The right answer is proxyUrl, either as a string or as a { url, when } object for runtimes other than the browser.
Budgeting from the pricing comparison column instead of the unit prices. With Veo 3 that understates the cost by twenty percent, as described above.
Treating output file URLs as private. By default they are public to anyone who knows the address, and they disappear once retention runs out. If an image is going into a product, download it and store it yourself, and control access through initial_acl in the X-Fal-Object-Lifecycle-Preference header.
Relying on a single gallery model version with no fallback. Models marked as partner models are hosted by third parties and their availability is managed by the partner, and standard percentage discounts do not apply to them.
FAQ
Does fal have a free plan?
Not in the classic sense. Billing is prepaid: you buy credits, which expire 365 days after purchase. Free credits and coupons do exist, but the vendor does not publish their size and describes their validity as variable, from a week to a year. Once the balance drops below the lock threshold the account is locked and requests are rejected.
Which client should I pick, JavaScript or Python?
Both talk to the same API, so the rest of your stack decides. The Python client is newer, has a fuller set of options for run and fits batch processing better. The JavaScript client, on the other hand, has @fal-ai/server-proxy with ready made handlers for Next.js, Express, Hono, SvelteKit and Remix, which simplifies hiding the key from the browser. Just remember the two packages carry different licences: MIT on the npm side, Apache 2.0 on the PyPI side.
When is run enough and when do I need subscribe?
run suits fast image models in scripts and prototypes, where a dropped connection costs only a repeat. subscribe gives the same blocking interface but rests on the queue, so you get server side retries, a queue position and logs. For anything taking minutes, submit with a webhook makes more sense anyway.
Can I run fal on my own infrastructure?
No. Only the client libraries are open, while the platform itself stays closed and has no locally installed variant. The dependency therefore covers not just the infrastructure but also the model gallery and whether a given model version will still be reachable under the same identifier in a year.
What does start_timeout do and how does it differ from timeout?
start_timeout, sent as the x-fal-request-timeout header, is a server side deadline for computation to begin and covers queue wait, retries and routing. Exceeding it returns a 504. The timeout in the Python client is an ordinary client side HTTP connection limit and has no effect on the server. In the JavaScript client, fal.run() supports startTimeout only.
Will I be charged for a failed request?
For server errors of 500 and above, never. For queue wait, also never. The vendor's FAQ, however, allows a charge on client side errors such as a 422 on invalid input, if a runner had already consumed GPU time. The pricing page says something more general in the same place, so the two documents do not agree.