Native controls and execution¶
Use typed configuration for network and transport controls, and explicit execution arguments for worker lifetime and event delivery. The complete native option inventory accounts for both supported libiperf versions, including aliases and CLI-only facilities.
The repository includes a runnable native-controls example with explicit destination, local binding, aggregate rate, optional event notices, a worker timeout and optional artifact output. Importing it generates no traffic; running it starts one benchmark against the requested server.
Select local addresses and devices¶
server identifies the remote destination. bind_address identifies an address
assigned to the local client host. bind_device names a local network device.
These controls answer different questions; selecting a device does not create
an address or route on it.
from iperf3_lib import Client, ClientConfig
config = ClientConfig(
server="127.0.0.1",
bind_address="127.0.0.1",
address_family="ipv4",
duration=2,
connect_timeout_ms=2_000,
)
result = Client(config).run(timeout=10)
print(result.ok, result.error)
For a particular non-loopback interface, replace the local address with an IP
actually assigned to that host. On a compatible Linux host, bind_device="eth0"
selects that named device; substitute its real name. Native support and
permissions still apply. Use explicit address/device fields instead of relying
on CLI %device shorthand.
client_port sets a data-stream source port, not the server's listening port or
the source port of the control connection. Reserve enough ports for the chosen
parallelism and avoid simultaneous reuse by other tests. Setting parallel
controls streams inside one native test; it does not make separate calls in the
same process reentrant.
For IPv6, choose address_family="ipv6" and IPv6 endpoints. A configured
flow_label requires TCP and that explicit family. Scoped/link-local addressing also
depends on the local device and native resolver; an IPv6 literal alone is not
a claim of available IPv6 connectivity.
The Python server's bind_host similarly selects a local address, not a
device name. See server configuration
for ServerConfig and named-device controls, and the
server example.
Tune TCP deliberately¶
config = ClientConfig(
server="127.0.0.1",
duration=2,
socket_buffer_bytes=131_072,
no_delay=True,
mss=1_200,
interval_seconds=0.5,
)
result = Client(config).run(timeout=10)
print(result.raw)
Socket-buffer sizes are requests; operating systems may cap or transform them.
Compare native requested and observed buffer fields instead of treating the
configuration value as a measured TCP window. congestion_control="cubic" is
an example only for a kernel that offers that algorithm; unavailable selections
must be reported as failures.
no_delay and mss accept TCP or SCTP; SCTP requires mss >= 512.
congestion_control and mptcp require TCP.
mptcp=True uses the worker and requires a compatible native build and kernel.
Neither a recognized native flag nor a successful TCP fallback establishes that
multiple MPTCP paths carried traffic.
Choose duration, bytes, or blocks¶
byte_test = ClientConfig(
server="127.0.0.1",
duration=None,
bytes_to_send=1_000_000,
)
block_test = ClientConfig(
server="127.0.0.1",
duration=None,
blocks_to_send=100,
blksize=4_096,
)
result = Client(byte_test).run(timeout=10)
Choose exactly one termination mode. Count-based runs require duration=None;
setting a count alongside the default duration is an error. A transfer target
is not an exact observed-byte assertion: protocol framing, complete native
blocks, omitted traffic and direction affect the measured scope. Inspect the
returned endpoint summaries.
duration=0 requests an unlimited native test. Use an explicit execution timeout
when that mode is intended. A measured duration, a native connection/receive
timeout and the worker's total execution timeout are separate controls.
Finite trial plans and sweeps have their own admission contract. Do not infer a finite active-time estimate from a byte or block count, or assume that a plan budget bounds a running native call.
Control offered load and UDP¶
from iperf3_lib import Protocol
from iperf3_lib.intent import RateIntent, parse_rate
config = ClientConfig(
server="127.0.0.1",
protocol=Protocol.UDP,
address_family="ipv4",
duration=2,
parallel=2,
blksize=1_200,
udp_counters_64bit=True,
dont_fragment=True,
pacing_timer_us=1_000,
)
intent = RateIntent(aggregate_bps_per_direction=parse_rate("10 Mbit/s"))
result = Client(config, rate_intent=intent).run(timeout=10)
rate remains a per-stream target. Aggregate intent divides one direction's
target across streams, preserving any unused remainder. burst_packets changes
the send pattern; pacing_timer_us changes the internal timer. fq_rate_bps
requests additional TCP/UDP socket-level pacing, where supported; SCTP rejects
that field, including zero. These requests do not
guarantee achieved rates or hard wire-traffic ceilings.
dont_fragment applies to IPv4 UDP; automatic family selection becomes IPv4 and
explicit IPv6 is rejected. gsro=True requests UDP GSO/GRO and requires
libiperf 3.21; do not assume that both peers or their kernels can use offload.
Keep offload and payload settings consistent when comparing trials.
For numeric DSCP with zero ECN bits, convert explicitly:
DSCP is six bits; tos is the entire eight-bit field. Validate application input
before shifting, and do not pass symbolic CLI names as tos.
Preserve native output and payload intent¶
config = ClientConfig(
server="127.0.0.1",
duration=2,
get_server_output=True,
extra_data="branch-office-baseline-v1",
title="upload",
repeating_payload=True,
)
result = Client(config).run(timeout=10)
print(result.raw)
The remote server determines the returned server-output format. Preserve its
native JSON/text without assuming a second normalized Result is available.
Use namespaced result.extensions for application artifact metadata; native
extra_data and title have separate meanings.
payload_file is a local file source or sink according to the native role and
direction. It is mutually exclusive with repeating_payload; it is not a
general-purpose or integrity-checked file transfer. zerocopy selects a TCP
send strategy; skip_rx_copy supports TCP/UDP receive-copy avoidance. SCTP
rejects both. These native facilities depend on the build/platform and may affect
comparability.
Observe events and bound a run¶
from iperf3_lib.events import NativeEvent
def on_event(event: NativeEvent) -> None:
print(event.sequence, event.kind, event.received_at_seconds)
config = ClientConfig(server="127.0.0.1", duration=2, json_stream=True)
result = Client(config).run(timeout=10, on_event=on_event)
NativeEvent contains kind, copied data, a sequence, and
received_at_seconds. The receipt time is not the interval's native measurement
boundary. The callback runs in the calling Python thread (arun() uses its
executor thread), never inside a native C callback. Both sides of the transport
use queues with capacity 256. Keep callbacks short; sequence gaps and
result.extensions["iperf3_lib.event_delivery"] report delivery loss through
emitted, dropped, and queue_capacity. A complete result remains the basis
for artifacts and final analysis; callbacks do not replace it. Native 3.21
streaming also enables its full-output facility. Native 3.19.1 has no such
facility: the wrapper explicitly labels raw as reconstructed_events and
retains the original event envelopes. Fields absent from those envelopes cannot
be represented as a complete original native document.
The marker and envelopes are in
result.extensions["iperf3_lib.native_json"] as representation and events.
The execution.reconstructed_json diagnostic identifies reconstructed capture.
Full native-document capture needs no reconstruction extension. Bounded live
delivery queues do not bound memory used for the retained intervals/result.
After a callback raises, further delivery stops. The active native run is
allowed to finish, and the callback error is then raised. An explicit timeout
terminates and reaps the worker and raises TimeoutError. Event loss does not
change the independently captured result evidence.
An independent watchdog terminates/reaps the worker at its deadline even if a Python callback is blocked. The caller still cannot regain control until that callback returns. Keep user work short or hand it off to an application-owned queue.
An explicit timeout or on_event callback selects the isolated Python/CFFI
worker even for a basic client. json_stream=True also selects that path.
The worker uses the installed Python package and shared library, not an
iperf3 executable. Native parser and process-global state remain inside its
process. Basic direct calls still require serialization within the caller's
process; starting multiple ordinary clients does not select isolation by itself.
The execution timeout bounds the worker operation. Native
connect_timeout_ms, receive_timeout_ms, send_timeout_ms, and
control_keepalive affect particular connection states and do not provide the
same lifetime guarantee. Consult the API contract for
timeout, callback-failure and async behavior rather than assuming that an
arbitrary asyncio.wait_for cancels a direct native call.
SCTP and host-specific settings¶
sctp_streams and sctp_bind_addresses configure SCTP associations, not TCP
streams. They require protocol=Protocol.SCTP and a compatible build/kernel.
sctp_bind_addresses is a tuple of address strings; address-family constraints
remain explicit.
affinity selects a local CPU inside the worker. server_affinity additionally
requests a CPU at the remote endpoint and requires an explicit local affinity.
CPU numbering and the CPUs allowed to a container or service vary by host.
Keep those settings in benchmark methodology instead of copying a fixed CPU
number between machines.
Authentication uses explicit client/server configuration and native OpenSSL
support. Set client username and rsa_public_key_path together, then supply
the password separately with Client(config, password=...) or the
IPERF3_PASSWORD environment variable. Passwords are not ClientConfig fields
and are not included in artifacts; username and key path remain request
metadata, while key contents are absent. Do not put reusable credentials in
extra_data, artifact extensions, labels or event handlers. See the complete
configuration contract before enabling it.
Client use_pkcs1_padding=True is rejected with libiperf 3.21, whose legacy
padding flag is server-only. The flag is accepted for clients with libiperf
3.19.1; matching authentication support is still required at both endpoints.
The tested 3.19.1/OpenSSL 3 build fails authentication even with valid
credentials because of an upstream encryption bug. Use the qualified 3.21
build for authenticated benchmarks; see the
native authentication limitation
for the reproduced behavior and build-specific scope.
Interpret applied-setting evidence¶
The result extension iperf3_lib.native_configuration records available
{field: {getter, value}} receipts. A matching getter confirms the native stored
request; it does not prove a kernel-applied buffer, an achieved pacing rate or
network-device behavior. Effective-setting provenance must retain that
distinction. Existing artifacts containing only the original configuration
fields remain readable; new requests preserve the expanded fields.
A configured advanced option also affects comparison eligibility. Matching requests do not establish matching native settings: every compared trial needs the relevant returned-value or getter receipts, including a peer that requested defaults.