Skip to content

Async

For asyncio servers, use async_dispatch. Methods can be async, and their awaits don't block the server.

import asyncio

from jsonrpcserver import Result, Success, async_dispatch, method


@method
async def ping() -> Result:
    return Success("pong")
>>> asyncio.run(async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}'))
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'

In a real server you're already inside a coroutine, so write response = await async_dispatch(request).

async_dispatch takes the same options as dispatch. async_dispatch_to_serializable and async_dispatch_to_response are the async versions of the other two functions.

Plain methods

Plain functions work as methods too:

@method
def add(a: int, b: int) -> Result:
    return Success(a + b)
>>> asyncio.run(
...     async_dispatch('{"jsonrpc": "2.0", "method": "add", "params": [2, 3], "id": 1}')
... )
'{"jsonrpc": "2.0", "result": 5, "id": 1}'

They run on the event loop, so a slow one holds up every other request. Make slow methods async, or move the work to a thread with await asyncio.to_thread(...).

New in 5.0.10

In 5.0.9, a plain method called through async_dispatch gives an Internal error. There, make every method async def.

It doesn't work the other way round. Async methods don't work with the synchronous dispatch. The client gets an Internal error, and the log says to use async_dispatch.

Batches run concurrently

The requests in a batch run at the same time. These ten requests take about a tenth of a second, not one second:

import json
import time


@method
async def wait() -> Result:
    await asyncio.sleep(0.1)
    return Success()


batch = json.dumps([{"jsonrpc": "2.0", "method": "wait", "id": n} for n in range(10)])
>>> start = time.monotonic()
>>> responses = json.loads(asyncio.run(async_dispatch(batch)))
>>> len(responses)
10
>>> time.monotonic() - start < 0.5
True

The responses still come back in the same order as the requests.

Every request in a batch runs at once. max_batch_size, 100 by default, limits how many a batch can hold. See Security.

Limit how many run at once

If each request opens a database connection or calls a rate-limited API, a batch of 100 still means 100 at once. max_concurrency makes the rest wait their turn. With a limit of 2, the same ten requests take about half a second:

>>> start = time.monotonic()
>>> responses = json.loads(asyncio.run(async_dispatch(batch, max_concurrency=2)))
>>> len(responses)
10
>>> 0.4 < time.monotonic() - start < 2
True

It's on every async dispatch function. The sync ones don't have it, because dispatch runs a batch one request at a time.

New in 6.0

max_concurrency.

Notifications

A notification is a request with no id. The spec says not to respond to it, so async_dispatch gives an empty string:

>>> asyncio.run(async_dispatch('{"jsonrpc": "2.0", "method": "ping"}'))
''

With HTTP you have to send something, so send an empty body with status 204. With websockets or a message queue, you can skip sending:

from typing import Awaitable, Callable


async def handle(request: str, send: Callable[[str], Awaitable[None]]) -> None:
    if response := await async_dispatch(request):
        await send(response)

The framework examples include aiohttp, FastAPI, Sanic, Tornado, websockets and ZeroMQ's asyncio API.