Request timeouts

Web endpoint (a.k.a webhook) requests should complete quickly, ideally within a few seconds. All webhook Function types (webhook, asgi, wsgi) have a maximum HTTP request timeout of 150 seconds enforced. However, the underlying Modal function can have a longer timeout.

In case the function takes more than 150s to complete, a HTTP status 301 redirect response is returned pointing at the original URL with an appended __modal_function_call_id=fc-123abc query parameter corresponding to the triggered webhook. Most web browsers allow for up to 20 such redirects, effectively allowing up to 50 min (20 * 150s) runtime for webhooks before the request times out. Some libraries and tools might require you to add a flag or option in order to follow redirects automatically, e.g. curl -L ... or http --follow ....

The eventual landing URL can be reloaded without triggering a new request, and will instead fetch the result of the original call.

wait_for_response=False

In cases when you want a webhook to trigger and return the result URL immediately, you can add the wait_for_response=False flag to your webhook/asgi/wsgi decorator. The decorated function will then be immediately triggered and a 202 accepted HTTP response will be returned, along with a JSON payload containing the aforementioned result URL: {"result_url": "...?__modal_function_call_id=..."}. This can be useful if you for example want to store the result URL somewhere for later access, or immediately redirect a web browser to a URL that won’t trigger a new function call on manual refresh.

Polling solutions

Sometimes it can be useful to be able to poll for results rather than wait for a long running HTTP request. The easiest way to do this is to have your webhook spawn a modal.Function call and return the function call id that another endpoint can use to poll the submitted function’s status. Here is an example:

import fastapi
from modal.functions import FunctionCall

web_app = fastapi.FastAPI()

@stub.asgi()
def fastapi_app():
    return web_app

@stub.function()
def slow_operation():
    ...

@web_app.post("/webhook")
async def accept_job(request: fastapi.Request):
    call = slow_operation.spawn()
    return {"call_id": call.object_id}


@web_app.get("/result/{call_id}")
async def poll_results(call_id: str):
    function_call = FunctionCall.from_id(call_id)
    try:
        return function_call.get(timeout=0)
    except TimeoutError:
        http_accepted_code = 202
        return fastapi.responses.JSONResponse({}, status_code=http_accepted_code)

Document OCR Web App is an example that uses this pattern.