PrefectHQ/fastmcp · error

Tool {func_name!r}: timeout cannot be enforced when run_in_t

Error message

Tool {func_name!r}: timeout cannot be enforced when run_in_thread=False on a sync function. Inline execution has no cancellation checkpoints, so the timeout would be a no-op. Either drop the timeout or remove run_in_thread=False and accept worker-thread dispatch.

What it means

For synchronous functions, a timeout is enforced by anyio.fail_after only when the function runs in a worker thread (run_in_thread=True), because inline sync execution blocks the event loop with no cancellation checkpoints. This ValueError rejects combinations where a timeout would be silently ignored.

Source

Thrown at fastmcp_slim/fastmcp/tools/function_tool.py:315

        parsed_fn = ParsedFunction.from_function(fn)
        func_name = metadata.name or parsed_fn.name

        if func_name == "<lambda>":
            raise ValueError("You must provide a name for lambda functions")

        # Inline sync execution has no cancellation checkpoints, so
        # anyio.fail_after cannot preempt the call — the timeout would be
        # silently ignored. Reject the combination so users make an
        # explicit choice. Async generators are async even though
        # is_coroutine_function returns False for them; the generator's
        # iteration has checkpoints, so timeout enforcement still works.
        if (
            metadata.timeout is not None
            and not metadata.run_in_thread
            and not is_coroutine_function(fn)
            and not inspect.isasyncgenfunction(fn)
        ):
            raise ValueError(
                f"Tool {func_name!r}: timeout cannot be enforced when "
                "run_in_thread=False on a sync function. Inline execution has "
                "no cancellation checkpoints, so the timeout would be a no-op. "
                "Either drop the timeout or remove run_in_thread=False and "
                "accept worker-thread dispatch."
            )

        # Normalize task to TaskConfig
        task_value = metadata.task
        if task_value is None:
            task_config = TaskConfig(mode="forbidden")
        elif isinstance(task_value, bool):
            task_config = TaskConfig.from_bool(task_value)
        else:
            task_config = task_value
        task_config.validate_function(fn, func_name)

        # Handle output_schema

View on GitHub (pinned to 1f02114297)

Solutions

  1. Drop the timeout parameter if the sync function must run inline
  2. Remove run_in_thread=False so the function runs in a worker thread where the timeout is enforced
  3. Make fn async (async def) so cancellation checkpoints exist

Example fix

// before
FunctionTool.from_function(sync_fn, timeout=10.0, run_in_thread=False)
// after
FunctionTool.from_function(sync_fn, timeout=10.0)  # default run_in_thread=True
Defensive patterns

Strategy: validation

Validate before calling

import inspect, asyncio
def check_timeout_viable(fn, timeout=None, run_in_thread=True):
    if timeout is not None and not run_in_thread and not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn):
        raise ValueError('timeout unenforceable: enable run_in_thread or drop timeout')

Type guard

def timeout_would_be_noop(fn, run_in_thread: bool) -> bool:
    return (not run_in_thread
            and not inspect.iscoroutinefunction(fn)
            and not inspect.isasyncgenfunction(fn))

Try / catch

try:
    tool = FunctionTool.from_function(fn, timeout=t, run_in_thread=False)
except ValueError as e:
    if 'timeout cannot be enforced' in str(e):
        tool = FunctionTool.from_function(fn, timeout=t)  # allow thread dispatch

Prevention

When it happens

Trigger: FunctionTool.from_function(fn, timeout=5.0, run_in_thread=False) where fn is a plain sync function (not a coroutine function and not an async generator function).

Common situations: Users explicitly disabling thread dispatch for performance while keeping a timeout configured; defaults inherited from metadata that set timeout without checking run_in_thread; copying a config that assumed async fn.

Understand the failure class

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/5697292738039fc4. Report an issue: GitHub.