from forward_sdk import ForwardClient
client = ForwardClient(
"https://fwd.app",
username="abcd-efghi-jklm",
password="secret",
verify=True,
timeout=300.0,
retries=4,
rate_limit_rpm="auto",
network_id="101",
snapshot_id=None,
user_agent="my-integration/1.2",
)ForwardClient.from_env() reads these, and keyword arguments override them:
| Variable | Meaning |
|---|---|
FORWARD_URL |
Instance URL. Required. |
FORWARD_USERNAME |
API token access key, or a login name. |
FORWARD_PASSWORD |
API token secret, or a password. |
FORWARD_NETWORK_ID |
Default network. |
FORWARD_SNAPSHOT_ID |
Default snapshot. |
FORWARD_VERIFY_TLS |
false disables certificate verification. |
FORWARD_TIMEOUT |
Read timeout in seconds. |
FORWARD_RETRIES |
Retry count. |
FORWARD_RATE_LIMIT_RPM |
Requests per minute. |
FORWARD_USER_AGENT |
Extra User-Agent token. |
These names match the Forward NetBox and Nautobot plugins, so an existing deployment's environment works unchanged.
Pass either the instance URL or the API root; /api is appended only when it is
not already there.
timeout accepts a read deadline in seconds, or a full httpx.Timeout for
separate connect, read, write and pool limits. The default allows ten seconds to
connect and five minutes to read, because a large query legitimately takes
minutes. Streaming responses get a longer read timeout, since Forward may pause
between chunks while it works.
retries accepts a count, or a RetryPolicy for full control:
from forward_sdk.config import RetryPolicy
client = ForwardClient(
"https://fwd.app",
retries=RetryPolicy(
max_attempts=6,
backoff_base=0.5,
backoff_cap=30.0,
max_retry_after=120.0,
),
)max_retry_after caps an honoured Retry-After, so an unreasonable server
value cannot stall a caller indefinitely.
rate_limit_rpm="auto", the default, paces requests at 1800 per minute against
Forward's hosted service and leaves self-hosted deployments alone. Pass an
integer to set a rate, or None to disable.
Three things about Forward's limiter are worth knowing, because they change how much the number matters.
The budget is per authenticated user. Forward counts requests per user per
minute in a sliding window, not per client, per host or per connection. Every
process authenticating with the same credentials spends from one allowance, so
a single client's pacing cannot protect a fleet. Pass any object with an
acquire() method as throttle= to plug in a shared counter; one integration
uses a Django cache so its workers share a budget across processes.
Overshooting stalls you rather than slowing you. Exceeding the limit does
not delay the request. It blocks the user for a lockout period and answers with
429 and a Retry-After carrying the remaining block. The lockout defaults to
one minute and an administrator can set it as high as sixty. The SDK honours
Retry-After, so a client waits correctly, but it waits: a breach costs a sync
its remaining runtime, not a few hundred milliseconds.
The ceiling is a setting, not a constant. It is an org property defaulting
to 2000 per minute, adjustable from 1 to 10,000, and on self-hosted deployments
the whole limiter ships disabled and an administrator decides whether to enable
it. The SDK cannot query any of this, so 1800 is headroom against the default,
not a reading of your ceiling. If you run many workers, or your administrator
has lowered the number, set rate_limit_rpm yourself.
verify defaults to True and accepts a CA bundle path. Prefer supplying the
bundle over disabling verification.
trust_env=True (the default) honours HTTPS_PROXY and NO_PROXY. Pass
proxy= to set one explicitly.
client.counters # requests, retries, 429s, sleep time, NQE rows and pagesPass hooks= with a subclass of forward_sdk.telemetry.Hooks to observe each
request, response, retry and pause. Override only the methods you need. A hook
that raises is logged and ignored rather than failing the request it was
watching.
The SDK logs to the forward_sdk logger at debug level, and never logs
credentials or bodies.
One thing is cached: the NQE query index, the map of library path to query,
because resolving a batch of paths would otherwise re-fetch the whole library
once per path. Any write through client.nqe.repo clears it.
Nothing else is. In particular a snapshot is resolved on every call that needs
one, including latest_processed and latest_collected. That is deliberate.
Caching a snapshot means deciding how long "latest" stays true, and only the
caller knows: a long sync wants one snapshot for the whole run, while a
dashboard polling for new data wants the opposite, and a wrong guess here is
silent, because a stale snapshot returns real data from the wrong point in time.
The cost is real, so plan for it. An integration migrating onto this SDK went from one snapshot request per sync to one per slice, because its planner resolved a snapshot inside a thread pool and the client it replaced had cached that itself. Nothing failed and no test noticed. It showed up only as request volume against a live account.
If you know an invariant the SDK cannot assume, tell it, with
snapshot_cache_ttl. It is off by default and takes a number of seconds:
# This sync pins one point in time for its whole run, so resolving "latest"
# once is right here even though it would not be in general.
client = ForwardClient.from_env(snapshot_cache_ttl=600)That covers latest_processed and latest_collected_id, keyed by the exact
question asked, so two different tag scopes stay two different answers. A
network with no processed snapshot is cached as an answer too, since re-asking
would spend the budget the setting exists to save.
Uploading a snapshot through the same client clears it, because that is the
event that makes a cached answer wrong. For anything the SDK cannot see, such
as an upload from another process or a snapshot that finished while you were
running, call client.snapshots.clear_cache().
Leave it off if "latest" genuinely has to mean latest. A stale snapshot does not raise. It returns real data from the wrong moment, which is why this is a setting rather than a default.
Read client.counters for cache_hits and cache_misses on the one cache
that exists, and for the request counts that would reveal this kind of change.
A redundant request is not free, because the budget above is spent per user across every process using those credentials, and exhausting it locks the user out rather than slowing them down. So caching is not a latency optimisation here. It is how a long sync stays inside an allowance it shares with everything else running under the same account.
The integration that went from one snapshot request per sync to one per slice
did not get slower in any way a test could see. It simply started spending a
larger share of a budget it did not own alone. Look at http_attempts and
attempts_per_minute after a change to how often you resolve something, not
just at whether the results are still correct.
A synchronous ForwardClient is safe to share across threads. An
AsyncForwardClient belongs to the event loop that created it.