Tune performance¶
This guide covers the knobs mde-client exposes for throughput, latency, and memory pressure. The defaults are tuned for moderate tenants; large tenants and batch pipelines often benefit from explicit tuning.
Pick the right materialization¶
Each terminal method on a results wrapper has different cost characteristics:
| Method | Best for | Trade-off |
|---|---|---|
to_arrow() |
Joining/transforming inside the package | Lowest overhead — wrappers cache Arrow internally |
to_polars() |
DataFrame analysis | Zero-copy from Arrow; ideal once you need a DataFrame |
to_dicts() |
Python-native pipelines, small payloads | Materialises every row as dict; highest memory cost |
to_json() |
Logging, serialisation | Encodes the dict view; do not use as a transit format |
If you call multiple terminal methods on the same wrapper, the underlying Arrow data is fetched once and reused. Call refresh() explicitly when you want a re-fetch.
Control pagination¶
Collection endpoints auto-paginate until exhausted. Two knobs change that:
from mde_client.endpoints.machines import MachinesQuery
# Cap results: a single page of at most 200 records.
q = MachinesQuery(top=200)
rows = client.machines.get_all(q).to_polars()
# Increase per-page size for fewer round-trips on big lists.
q = MachinesQuery(page_size=1000)
rows = client.machines.get_all(q).to_polars()
- Setting
top(orskip) disables auto-pagination and issues a single request. page_sizeincreases the per-page batch without disabling pagination.- Larger
page_sizereduces request count but increases per-response memory and latency.
Tune the HTTP client¶
Inject an httpx.Client with explicit timeouts, retries, and connection limits:
import httpx
from mde_client import MDEClient
http = httpx.Client(
base_url="https://api.securitycenter.microsoft.com",
timeout=httpx.Timeout(30.0, connect=5.0),
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
# transport=httpx.HTTPTransport(retries=3), # transport-level connect retries
)
with MDEClient(tenant_id, client_id, client_secret, http_client=http) as client:
...
The client takes ownership when you do not inject one. If you do inject, you also own the lifecycle; close it when you are done.
Tune ViaFiles for export-backed endpoints¶
ViaFilesConfig exposes the knobs that matter for file-export workflows:
| Field | What it controls |
|---|---|
download_workers |
Concurrency for blob downloads |
client_timeout |
Per-blob HTTP timeout |
retry_attempts |
How many times a blob is retried on failure |
retry_delay_seconds |
Delay between retries |
download_chunk_size |
Streaming read granularity (bytes) |
parse_batch_size |
Records per Arrow batch during NDJSON parsing |
Defaults are conservative. For large tenants:
- Raise
download_workers(8–16) on a fast network with spare CPU. - Raise
parse_batch_sizeto reduce Arrow overhead per batch. - Raise
client_timeoutwhen blobs are large or the network has high tail latency.
Reuse the client across calls¶
MDEClient owns one connection pool and one MSAL token cache. Reuse a single instance per process:
# Good
with MDEClient(...) as client:
machines = client.machines.get_all().to_polars()
alerts = client.alerts.get_all().to_polars()
# Bad — closes the pool, drops the token cache, repeats MSAL flow.
def get_machines():
with MDEClient(...) as c: ...
def get_alerts():
with MDEClient(...) as c: ...
If you do need a long-running process (worker, server, scheduler), inject a persistent msal.SerializableTokenCache so cached tokens survive process restarts. See Inject a custom HTTP client or token cache.
Refresh deliberately¶
A results wrapper caches the fetched Arrow data the first time you materialize it. Subsequent terminal calls reuse the cache:
results = client.machines.get_all()
df1 = results.to_polars() # network fetch
df2 = results.to_polars() # cached, no fetch
results.refresh()
df3 = results.to_polars() # network fetch again
In long-running pipelines, hold the wrapper and call refresh() on a schedule rather than instantiating new endpoint queries.