@app.web_endpoint
Expose a Python function through HTTP call routing: each incoming request becomes one invocation of the handler. Usable alone (it registers a function with default resources) or **stacked with @app.function / @app.concurrent in either order** — decorator order is not significant.
@app.web_endpoint(
*,
path: Optional[str] = None,
methods: Optional[Sequence[str]] = None,
name: Optional[str] = None,
min_replicas: Optional[int] = None,
max_replicas: Optional[int] = None,
max_concurrent_inputs: Optional[int] = None,
scale_down_idle_seconds: Optional[int] = None,
max_queue_depth: Optional[int] = None,
reserved_replicas: Optional[int] = None,
public: bool = False,
autoscale: Optional[bladekit.Autoscale] = None,
) -> FunctionpathOptional[str]Default:/<function name>Route path. Must start with
/and be at most 1024 characters, else raisesInvalidArgumentError.methodsOptional[Sequence[str]]Default:("POST",)Accepted HTTP methods. Upper-cased and validated against
GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD; must be unique, non-empty, and at most 8 entries.nameOptional[str]Default:NoneEndpoint name in the deployment manifest. Defaults to the function's name.
min_replicasOptional[int]Default:NoneReplica floor (0–50).
0allows scale-to-zero when idle. Wire default is0.max_replicasOptional[int]Default:NoneReplica ceiling (1–50). Wire default is
1. Must be>= min_replicas.max_concurrent_inputsOptional[int]Default:NonePer-replica in-flight HTTP call ceiling (1–100). Usually set with
@app.concurrentinstead; wire default is1.scale_down_idle_secondsOptional[int]Default:NoneIdle seconds before scale-down (30–3600). Wire default is
300. Governs both the descent tomin_replicasand the floor of the surplus-shedding stabilization window.max_queue_depthOptional[int]Default:NoneIngress queue bound (0–10000). Past this many requests waiting for a replica slot, new ones get an immediate
503 ENDPOINT_QUEUE_FULL+Retry-Afterinstead of waiting. Wire default is0= unbounded (the historical behavior).reserved_replicasOptional[int]Default:NoneAlways-on floor served on non-interruptible capacity (0–
max_replicas).publicboolDefault:FalseEndpoints are private by default — the
/serveingress requires a valid user token.public=Trueopts out (no auth), so the URL opens directly.autoscaleOptional[bladekit.Autoscale]Default:NoneAutoscaling-v2 policy.
vllm_target_waitinginside it is only valid on@app.web_serverand is rejected here at decorator time.
The handler receives a single argument: the parsed JSON body (when the request is application/json with a JSON object body) — faithful to Modal's ergonomics, so item["prompt"] works directly. The argument is a dict subclass that also carries the full HTTP envelope as attributes: item.method, item.path, item.query_string, item.headers, item.body (raw bytes), item.json, item.endpoint_id, item.app_id, item.deployment_id, plus item.request for the whole envelope as a dict.
import bladekit
app = bladekit.app("api")
@app.web_endpoint(methods=["POST"])
def predict(item):
prompt = item["prompt"] # item IS the decoded JSON body
return {"echo": prompt}@app.web_endpoint(methods=["POST"], max_replicas=4)
@app.function(gpu="RTX-A4500", image=bladekit.Image.from_catalog("fabric/torch"))
def classify(item):
...@app.web_server
Serve a long-lived HTTP server running inside the container rather than a handler called per request. The decorated function is invoked once at container boot to start the server — typically a non-blocking subprocess.Popen(["vllm", "serve", …]). AI Fabric probes the loopback until it accepts connections (up to startup_timeout), then forwards every request whose path matches the endpoint's path prefix — regardless of method, since the server owns its own routing. This is the path that serves vLLM and other OpenAI-compatible servers with continuous batching.
@app.web_server(
*,
port: Optional[int] = None,
startup_timeout: int = 300,
path: str = "/",
name: Optional[str] = None,
min_replicas: Optional[int] = None,
max_replicas: Optional[int] = None,
max_concurrent_inputs: Optional[int] = None,
scale_down_idle_seconds: Optional[int] = None,
max_queue_depth: Optional[int] = None,
reserved_replicas: Optional[int] = None,
public: bool = False,
autoscale: Optional[bladekit.Autoscale] = None,
snapshot: bool = False,
) -> FunctionportOptional[int]Default:NonePassing a fixed port is rejected (
InvalidArgumentError). Each container is assigned a node-unique loopback port viaFABRIC_SERVER_PORT; your server must bind$FABRIC_SERVER_PORT.startup_timeoutintDefault:300Seconds the runner probes the loopback for readiness (10–1800). The replica fails if the server never answers within the budget.
pathstrDefault:"/"Path prefix the endpoint owns (all sub-paths, every method).
/captures the whole app's URL space.nameOptional[str]Default:NoneEndpoint name; defaults to the function's name.
snapshotboolDefault:FalseOpt into GPU checkpoint/restore (checkpoint a warm replica at drain, restore on scale-up, fall back to a normal boot on error). Valid only on a single-GPU
web_server(gpu_count == 1); otherwise raisesInvalidArgumentErrorat deploy build time.
min_replicas, max_replicas, max_concurrent_inputs, scale_down_idle_seconds, max_queue_depth, reserved_replicas, public and autoscale behave exactly as on @app.web_endpoint. Combine with @app.concurrent(max_inputs=N) so multiple requests are forwarded concurrently — this is what enables server-side continuous batching (e.g. vLLM).
import os, subprocess
@app.web_server(startup_timeout=600)
@app.concurrent(max_inputs=32)
@app.function(gpu="RTX-A4500", image=bladekit.Image.from_catalog("fabric/vllm"))
def serve():
port = os.environ["FABRIC_SERVER_PORT"] # bind the assigned port
subprocess.Popen(
["vllm", "serve", "meta-llama/Llama-3.1-8B-Instruct", "--port", port],
)@app.asgi_app
Mount an ASGI app (FastAPI, Starlette, …) as an endpoint. The decorated function returns the ASGI app object; at boot the runner allocates a loopback port, starts uvicorn on the returned app, and forwards each request to it. uvicorn must be present in the image. The endpoint captures every method under path. Stackable with @app.function / @app.concurrent like @app.web_endpoint.
@app.asgi_app(
*,
path: str = "/",
startup_timeout: int = 300,
name: Optional[str] = None,
min_replicas: Optional[int] = None,
max_replicas: Optional[int] = None,
max_concurrent_inputs: Optional[int] = None,
scale_down_idle_seconds: Optional[int] = None,
max_queue_depth: Optional[int] = None,
reserved_replicas: Optional[int] = None,
public: bool = False,
autoscale: Optional[bladekit.Autoscale] = None,
) -> FunctionpathstrDefault:"/"Path prefix the ASGI app owns (all sub-paths, every method).
startup_timeoutintDefault:300Seconds the runner probes uvicorn's loopback for readiness (10–1800) before marking the replica ready.
nameOptional[str]Default:NoneEndpoint name; defaults to the function's name.
min_replicas, max_replicas, max_concurrent_inputs, scale_down_idle_seconds, max_queue_depth, reserved_replicas, public and autoscale behave exactly as on @app.web_endpoint. Set public=True for an app that serves a browser UI (or WebRTC signaling) so the URL opens without a token.
@app.asgi_app(public=True)
@app.function(image=bladekit.Image.from_catalog("fabric/base"))
def web():
from fastapi import FastAPI
api = FastAPI()
@api.get("/health")
def health():
return {"ok": True}
return api@app.concurrent
Set the per-replica HTTP concurrency for an endpoint. max_inputs is the admission ceiling per replica; the optional target_inputs is what the autoscaler aims for (autoscaling v2): the fleet is sized to hold roughly target_inputs in-flight calls per replica, and the margin up to max_inputs absorbs bursts while new replicas boot. Stacks with the endpoint decorators and @app.function in either order.
@app.concurrent(
*,
max_inputs: Optional[int] = None,
max_concurrent_inputs: Optional[int] = None,
target_inputs: Optional[int] = None,
) -> Functionmax_inputsOptional[int]Default:NonePer-replica admission ceiling (1–100). Required unless
max_concurrent_inputsis given.max_concurrent_inputsOptional[int]Default:NoneAlias for
max_inputs— exactly one of the two must be provided, else raisesInvalidArgumentError.target_inputsOptional[int]Default:NoneIn-flight calls per replica the autoscaler targets (1–100). Cannot exceed
max_inputs.
@app.web_endpoint(methods=["POST"])
@app.concurrent(max_inputs=32, target_inputs=24)
@app.function(gpu="RTX-A4500", image=bladekit.Image.from_catalog("fabric/torch"))
def infer(item):
...Endpoints on @app.cls methods
@app.web_endpoint, @app.web_server and @app.asgi_app can decorate a **method of an @app.cls class. This is fully supported: the endpoint method inherits the class's image and resources** (gpu, cpu, memory, timeout, idle_timeout, volumes, secrets, gpu_class, best_effort) for anything the method does not set itself — exactly like a plain @app.method. The runner instantiates the class once per container and routes requests to the method, so @bladekit.enter() model-load hooks run before the first call.
@app.cls(gpu="RTX-A4500", image=bladekit.Image.from_catalog("fabric/torch"))
class Classifier:
@bladekit.enter()
def load(self):
self.model = load_model() # once per container
@app.web_endpoint(methods=["POST"])
def predict(self, item):
return {"label": self.model(item["text"])}Reference from the Blade console · Updated .