Skip to main content
This guide maps Modal concepts to their fal equivalents and shows how to convert your code.

Concept Mapping

ModalfalNotes
@app.function()@fal.function()Standalone serverless functions
@app.cls()class MyApp(fal.App)Class-based apps (recommended)
@modal.method()@fal.endpoint("/")fal uses HTTP endpoints, not RPC
@modal.enter()def setup(self)Container startup hook
@modal.exit()def teardown(self)Container shutdown hook
modal.Image.debian_slim().pip_install(...)requirements = [...]Or use ContainerImage for Dockerfiles
modal.Image.from_dockerfile(...)ContainerImage.from_dockerfile(...)Custom container support
gpu="A100"machine_type = "GPU-A100"GPU selection
modal.Volume/data persistent storageMounted automatically on all runners
modal.Secretfal secrets setSecrets exposed as env vars
modal deployfal deployCLI deployment
Cls().method.remote(x=123)fal_client.subscribe(...)fal uses HTTP + queue, not RPC

Migration Path 1: Standalone Functions

If you use @app.function() in Modal, the closest fal equivalent is @fal.function().
If you use @app.cls() with @modal.enter() and @modal.method(), convert to a fal.App class.

Deploying

# Modal
modal deploy my_app.py

# fal
fal deploy my_app.py::TextToImage

Calling Your App

# Modal
TextToImage().generate.remote(prompt="a sunset")

# fal
import fal_client
result = fal_client.subscribe("your-username/text-to-image", arguments={
    "prompt": "a sunset"
})

Key Differences

HTTP Endpoints vs RPC

Modal uses .remote() to invoke functions — a Python-to-Python RPC call. fal uses HTTP endpoints that any language or tool can call. Your fal App exposes a REST API automatically. This means:
  • No .remote() calls — clients use fal_client.subscribe() or raw HTTP
  • Your endpoints accept and return JSON (use Pydantic models for validation)
  • The same endpoint works from Python, JavaScript, cURL, or any HTTP client

Queue-Based by Default

fal provides a persistent queue that handles retries, scaling, and load balancing automatically. When callers use subscribe or submit, requests go through this queue by default. Direct calls via run bypass the queue for lower overhead.

Container Images

Modal chains image methods (Image.debian_slim().pip_install(...).apt_install(...)). fal offers two approaches:
  • requirements list (simpler): Just list your pip packages
  • ContainerImage (full control): Bring your own Dockerfile
# Modal
image = (
    modal.Image.debian_slim()
    .apt_install("ffmpeg")
    .pip_install("torch", "diffusers")
)

# fal (simple)
class MyApp(fal.App):
    requirements = ["torch", "diffusers"]

# fal (Dockerfile)
from fal.container import ContainerImage

class MyApp(fal.App):
    image = ContainerImage.from_dockerfile_str("""
        FROM python:3.11
        RUN apt-get update && apt-get install -y ffmpeg
        RUN pip install torch diffusers
    """)

Volumes and Storage

Modal uses named Volume objects mounted at specific paths. fal provides /data — a persistent filesystem automatically mounted on every runner, shared across all your apps.
# Modal
volume = modal.Volume.from_name("model-cache")

@app.cls(volumes={"/cache": volume})
class MyModel:
    ...

# fal -- /data is always available, no configuration needed
class MyModel(fal.App):
    def setup(self):
        model_path = "/data/models/my-model.pt"
        if os.path.exists(model_path):
            self.model = torch.load(model_path)

Secrets

Modal uses modal.Secret.from_name(...) and attaches secrets to functions. fal exposes secrets as environment variables:
# Modal
modal secret create my-secret HF_TOKEN=hf_xxx

# fal
fal secrets set HF_TOKEN=hf_xxx
Both make secrets available via os.environ["HF_TOKEN"] in your code.