Advanced Remote Calls & Non-Blocking Execution
The @remote() decorator has one core role in FrameX: move execution-heavy work behind a separate execution boundary while keeping the call site stable.
Core Role
A plugin route can be lightweight at the API layer, while the work behind it is not.
That work may involve:
- blocking synchronous libraries
- long-running computation
- legacy code that is not fully async
- logic that should keep the same call form in local mode and Ray mode
If that work stays inline inside the plugin handler, the plugin becomes harder to isolate and harder to move across execution backends later.
Even though enabling Ray ensures that one plugin's heavy workload will not block other plugins, it does not prevent blocking inside a single plugin itself.
If some part of your plugin code performs a blocking operation, such as time.sleep, long-running computation, or a non-async library call, the entire plugin instance may become unresponsive.
@remote() gives that work a stable execution boundary without changing the surrounding plugin API.
If you are defining a plugin API, use @on_request(...). If you are defining execution work behind that API, use @remote().
Typical Uses
Use @remote() when you want one of these outcomes:
- keep blocking or heavy work out of the main plugin handler path
- keep the same call form in both local mode and Ray mode
- isolate execution-heavy logic without changing the plugin API surface
- prepare code that runs locally today but may run through Ray later
What This Looks Like
A plugin handler stays focused on request handling:
from framex.plugin import on_request, remote
@remote()
def heavy_job(x: int) -> int:
return x * 2
@on_request("/api/v1/demo/run")
async def run_job(x: int):
return await heavy_job.remote(x)
That is the main pattern: keep the API-facing handler small, and move execution-heavy work behind @remote().
Supported Shapes
The current implementation supports:
- plain functions
- instance methods
- class methods
The stable call form is:
await func.remote(...)
Execution Behavior
The same .remote(...) call uses different execution paths depending on the active adapter.
In local mode:
- async functions are awaited directly
- sync functions run through
asyncio.to_thread(...)
In Ray mode:
- the callable is wrapped through
ray.remote(...) .remote(...)executes through Ray
That is the key point: the call interface stays stable while the backend changes.
Ray backend setup is covered in Integrating Ray Engine.
More Examples
Plain Function
from framex.plugin import remote
@remote()
def heavy_job(x: int) -> int:
return x * 2
result = await heavy_job.remote(21)
Instance Method
from framex.plugin import remote
class Worker:
@remote()
def total(self, values: list[int]) -> int:
return sum(values)
count = await Worker().total.remote([1, 2, 3])
Class Method
from framex.plugin import remote
class Worker:
@classmethod
@remote()
def scale(cls, x: int) -> int:
return x * 10
value = await Worker.scale.remote(2)
Rule Of Thumb
Use @remote() when you need one callable interface with backend-dependent execution.
Use @on_request(...) for APIs. Use @remote() for the work those APIs call behind the scenes.