PyModbus
PyModbusDocs

Async Client

Use PyModbus with asyncio for concurrent Modbus reads across multiple devices, parallel operations, and non-blocking communication.

Async Modbus Client

PyModbus supports Python's asyncio for non-blocking Modbus communication. You get concurrent reads across multiple devices without threads.

Connect and Read

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def read_data():
    async with AsyncModbusTcpClient('192.168.1.100') as client:
        result = await client.read_holding_registers(0, 10, slave=1)
        if not result.isError():
            print(f"Data: {result.registers}")
        return result.registers

data = asyncio.run(read_data())

Log Modbus data automatically

TofuPilot records test results from your PyModbus scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

Parallel Reads with gather

Read from multiple devices at the same time. Each device gets its own connection and the reads run concurrently.

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def read_device(host, slave):
    async with AsyncModbusTcpClient(host) as client:
        result = await client.read_holding_registers(0, 10, slave)
        return host, result.registers if not result.isError() else None

async def read_multiple_devices():
    devices = [
        ('192.168.1.10', 1),
        ('192.168.1.11', 1),
        ('192.168.1.12', 1),
    ]

    tasks = [read_device(host, slave) for host, slave in devices]
    results = await asyncio.gather(*tasks)

    for host, data in results:
        print(f"{host}: {data}")

    return results

asyncio.run(read_multiple_devices())

Async Serial RTU

Async works with serial connections too. Useful when polling multiple slaves on the same bus.

import asyncio
from pymodbus.client import AsyncModbusSerialClient

async def async_rtu_example():
    client = AsyncModbusSerialClient(
        port='/dev/ttyUSB0',
        baudrate=9600,
        timeout=1
    )
    await client.connect()

    for slave_id in range(1, 5):
        result = await client.read_holding_registers(0, 10, slave=slave_id)
        if not result.isError():
            print(f"Slave {slave_id}: {result.registers}")

    client.close()

asyncio.run(async_rtu_example())

Async Context Manager

The AsyncModbusTcpClient context manager handles connect and close for you. Prefer this pattern to avoid leaked connections.

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def safe_read(host, address, count):
    async with AsyncModbusTcpClient(host, timeout=5) as client:
        result = await client.read_holding_registers(address, count)
        if not result.isError():
            return result.registers
    return None

asyncio.run(safe_read('192.168.1.100', 0, 10))

Retry with Timeout

Combine asyncio.wait_for with a retry loop for unreliable connections.

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def read_with_retry(host, address, count, max_retries=3):
    for attempt in range(max_retries):
        try:
            async with AsyncModbusTcpClient(host, timeout=5) as client:
                result = await asyncio.wait_for(
                    client.read_holding_registers(address, count),
                    timeout=3.0
                )
                if not result.isError():
                    return result.registers
                print(f"Attempt {attempt + 1} error: {result}")

        except asyncio.TimeoutError:
            print(f"Attempt {attempt + 1} timeout")
        except Exception as e:
            print(f"Attempt {attempt + 1} exception: {e}")

        if attempt < max_retries - 1:
            await asyncio.sleep(1)

    return None

Performance Comparison

Async shines when you're reading from multiple devices or making many requests through a single connection.

import asyncio
import time
from pymodbus.client import AsyncModbusTcpClient, ModbusTcpClient

async def benchmark_async(host, num_requests=100):
    start = time.time()
    async with AsyncModbusTcpClient(host) as client:
        tasks = [
            client.read_holding_registers(0, 10)
            for _ in range(num_requests)
        ]
        results = await asyncio.gather(*tasks)
    elapsed = time.time() - start
    successful = sum(1 for r in results if not r.isError())
    print(f"Async: {num_requests} requests in {elapsed:.2f}s ({num_requests/elapsed:.1f} req/s, {successful} ok)")

def benchmark_sync(host, num_requests=100):
    start = time.time()
    client = ModbusTcpClient(host)
    client.connect()
    successful = 0
    for _ in range(num_requests):
        result = client.read_holding_registers(0, 10)
        if not result.isError():
            successful += 1
    client.close()
    elapsed = time.time() - start
    print(f"Sync:  {num_requests} requests in {elapsed:.2f}s ({num_requests/elapsed:.1f} req/s, {successful} ok)")

asyncio.run(benchmark_async('192.168.1.100'))
benchmark_sync('192.168.1.100')

Async is most useful when polling multiple devices, reading at high frequency, or integrating Modbus into an async application (web server, event loop).