blackbull.env¶
blackbull.env
¶
Runtime configuration sourced from environment variables.
All server settings live in :class:Settings. Retrieve the current
configuration with :func:get_settings, which reads environment variables
once and returns an immutable snapshot.
Environment variables¶
BLACKBULL_ENV
production | development (default) | test
BB_WORKERS
Number of worker processes. 0 resolves to os.cpu_count().
Default: 1.
BB_MAX_CONNECTIONS
Maximum simultaneous TCP connections accepted per worker. When the
cap is reached, new connections receive HTTP/1.1 503 Service
Unavailable with Retry-After: 1 (a load-balancer-friendly
response, not a silent reset). 0 disables the cap and relies
on the OS file-descriptor limit instead. Default: 0 (uncapped).
Production deployments on untrusted hosts should set this to a
finite ceiling — 1024 is a sensible single-loop value; multi-worker
deployments multiply (so workers=8 × BB_MAX_CONNECTIONS=1024
→ 8K connections per process).
BB_STREAM_QUEUE_DEPTH
asyncio.Queue depth for HTTP/2 per-stream request-body events.
Limits memory growth when an ASGI handler is slower than the client.
Default: 64.
BB_WS_QUEUE_DEPTH
asyncio.Queue depth for WebSocket inbound events per connection.
Default: 256.
BB_ASYNC_LOGGING
1 | true | yes to enable; 0 | false | no to disable.
When enabled, a QueueHandler is installed on the blackbull logger
so that logger.debug/info calls in the event loop are non-blocking.
Default: true.
BB_ACCESS_LOG
1 | true | yes to enable; 0 | false | no to disable.
When disabled, the blackbull.access logger is silenced (level set to
WARNING) so no access log records are formatted or emitted. Useful in
production where a separate log aggregator consumes structured logs and the
per-request overhead of the access logger is undesirable.
Default: true.
BB_LOG_FORMAT
Async-logging sink format. json emits one structured JSON object per
line; anything else (default) keeps plain text.
Default: (plain).
BB_SYSLOG_ADDR
host:port of a syslog/UDP collector (e.g. 127.0.0.1:514). When set,
the async-logging sink ships records via a UDP SysLogHandler instead of
stderr. Composes with BB_LOG_FORMAT=json.
Default: (stderr sink).
BB_LOG_BATCH_SIZE
When > 1, the stderr async-logging sink coalesces up to this many
formatted lines into a single write(). 1 (default) is one write per
record. Ignored for the syslog sink.
Default: 1.
BB_LOG_BATCH_TIMEOUT_MS
Max milliseconds a partial log batch waits before flush. Only meaningful
when BB_LOG_BATCH_SIZE > 1.
Default: 5.
BB_SOCKET_BACKLOG
listen() backlog depth for the server socket. Increasing this reduces
silent connection drops during burst traffic when the accept loop falls
behind. Capped by net.core.somaxconn on Linux.
Default: 128 (matches the Linux net.core.somaxconn traditional
default). Bump to 4096 for production traffic — see
docs/reference/env-vars.md "Performance recommendations".
BB_SOCKET_SNDBUF
Kernel send-buffer size (bytes) set on each accepted TCP socket via
SO_SNDBUF. The kernel doubles the requested value internally.
Larger values improve throughput for large responses (≥64 kB).
0 leaves the kernel default unchanged.
Default: 0 (kernel default). 262144 (256 kB) is a common
production value — see docs/reference/env-vars.md.
BB_SOCKET_RCVBUF
Kernel receive-buffer size (bytes) set on each accepted TCP socket via
SO_RCVBUF. Same doubling rule as BB_SOCKET_SNDBUF.
0 leaves the kernel default unchanged.
Default: 0 (kernel default). 262144 (256 kB) is a common
production value — see docs/reference/env-vars.md.
BB_SOCKET_REUSEPORT
1 | true | yes to enable; 0 | false | no to disable.
When enabled and the OS supports SO_REUSEPORT, each worker binds its own
listening socket so the kernel distributes incoming connections across workers
independently, eliminating thundering-herd and improving CPU affinity.
Has no effect with a single worker or on platforms without SO_REUSEPORT.
Default: false (kernel default). Enable on multi-worker production
deployments — see docs/reference/env-vars.md.
BB_KEEP_ALIVE_TIMEOUT
Idle timeout (seconds) on a keep-alive HTTP/1.1 connection that is
awaiting the next request. Application-level timer; same
ghost-eviction guarantee as SO_KEEPALIVE without the per-accept
syscall cost. 0 disables the timer. Default: 5.0.
BB_TCP_USER_TIMEOUT_MS
TCP_USER_TIMEOUT value in milliseconds for accepted sockets
(Linux only). Forces a connection-level error if a peer fails to
ACK in this window — defends against dead-mid-write peers that
SO_KEEPALIVE misses. 0 leaves the kernel default
unchanged. Default: 0.
BB_HEADER_TIMEOUT
Maximum seconds the server will wait for a complete HTTP/1.1
request-header block (request-line + headers + CRLFCRLF). Primary
slowloris defence. When the deadline elapses the server returns
408 Request Timeout and closes. 0 disables.
Default: 10.0.
BB_BODY_TIMEOUT
Maximum seconds for the HTTP/1.1 request body to arrive once headers
are parsed. Mirrors BB_HEADER_TIMEOUT for the body half;
defeats slowloris-style Content-Length: N connections that drip
body bytes after the headers have arrived. 0 disables.
Default: 30.0.
BB_WRITE_TIMEOUT
Maximum seconds the server will wait for a single write to flush to
the peer (via StreamWriter.drain()). Defends against the
slow-read shape of slowloris: a client that reads 1 byte/sec
eventually fills the kernel send buffer and drain() would block
indefinitely. 0 disables. Default: 30.0.
BB_REQUEST_TIMEOUT
Maximum seconds a single request handler is allowed to run. Applied on
both protocols: HTTP/2 cancels the stream with RST_STREAM CANCEL; HTTP/1.1
emits 408 Request Timeout with Connection: close and closes the
connection (no keep-alive across a timed-out request). Prevents slow or
stalled handlers from holding stream / connection slots indefinitely.
0 disables the timeout. Default: 0 (disabled).
BB_HEADER_MAX_LINE
Maximum bytes in a single HTTP/1.1 request-line or header line.
Enforced before parsing so an attacker cannot exhaust memory with a
pathological 1 GB header. Default: 8192 (matches Apache
LimitRequestLine / nginx large_client_header_buffers).
BB_HEADER_MAX_TOTAL
Maximum total bytes in the entire HTTP/1.1 request header block
(request-line + all headers + CRLFCRLF). Default: 65536
(matches typical reverse-proxy defaults).
BB_BODY_CHUNK_SIZE
Slice size (bytes) for streaming an HTTP/1.1 Content-Length request
body to the ASGI app as successive http.request events instead of one
giant allocation. Default: 65536 (asyncio's StreamReader buffer).
Must be > 0.
BB_H2_INITIAL_WINDOW_SIZE
Per-stream flow-control window size (bytes) advertised to HTTP/2 peers in the
server's initial SETTINGS frame. Larger values allow peers to send more data
per stream before waiting for WINDOW_UPDATE.
Default: 65535 (RFC 9113 §6.9.2 default). 1048576 (1 MiB) is a
common tuned value for upload-heavy or multiplexed workloads — see
docs/reference/env-vars.md.
BB_H2_CONNECTION_WINDOW_SIZE
Connection-level flow-control window size (bytes) advertised to HTTP/2 peers
via an initial WINDOW_UPDATE on stream 0 after the SETTINGS handshake.
Must be ≥ 65535 (the RFC default); values below that are silently ignored.
Default: 65535 (RFC 9113 §6.9.2 minimum). 4194304 (4 MiB) is a
common tuned value to allow concurrent streams to share the connection
budget without head-of-line stalls — see docs/reference/env-vars.md.
BB_H2_MAX_CONCURRENT_STREAMS
Maximum number of HTTP/2 streams the server accepts at the same time per
connection, advertised to peers in the initial SETTINGS frame
(RFC 9113 §6.5.2 — SETTINGS_MAX_CONCURRENT_STREAMS, identifier 0x0003).
Incoming streams that would exceed this limit receive RST_STREAM
REFUSED_STREAM and are not dispatched to the application.
Default: 100.
BB_H2_ACTIVE_STREAMS_1W
Per-connection asyncio.Semaphore cap on running stream handlers
when workers == 1. Counterpart of BB_H2_ACTIVE_STREAMS for
the single-worker case (where one event loop sees all connections).
0 disables the cap. Default: 20.
BB_H2_ACTIVE_STREAMS
Per-connection asyncio.Semaphore cap on running stream handlers
when workers > 1. Newly-spawned stream tasks queue for the
semaphore instead of running immediately, which prevents one high-mux
connection from monopolising the event loop and starving other
connections on the same worker. 0 disables the cap (no upper
bound beyond BB_H2_MAX_CONCURRENT_STREAMS). Default: 20.
BB_H2_ENABLE_WEBSOCKET
Advertise SETTINGS_ENABLE_CONNECT_PROTOCOL=1 (RFC 8441 §3) so
peers may bootstrap WebSocket over HTTP/2 via Extended CONNECT.
Off by default — this path has fewer conformance tests than the
HTTP/1.1 upgrade path. Default: false.
BB_H2_WS_MAX_STREAMS_PER_CONNECTION
Maximum concurrent WebSocket (RFC 8441 Extended CONNECT) streams
per HTTP/2 connection. 0 disables the per-connection cap (no
upper bound beyond BB_H2_MAX_CONCURRENT_STREAMS). Only
meaningful when BB_H2_ENABLE_WEBSOCKET=1 — without that, no
WS-over-H2 streams are accepted at all. Defends against
stream-exhaustion DoS: without a per-connection cap, an attacker
can hold BB_H2_MAX_CONCURRENT_STREAMS idle WS streams open
per connection, multiplied by BB_MAX_CONNECTIONS (default 0 =
unbounded). Default: 5.
BB_WS_PERMESSAGE_DEFLATE
Negotiate permessage-deflate (RFC 7692) on incoming WebSocket
handshakes when the peer offers it. Matches modern browsers and
major WebSocket libraries. Default: true.
BB_WS_MAX_FRAME_PAYLOAD
Hard cap on the declared payload length (bytes) of a single
inbound WebSocket frame. RFC 6455 §5.2 allows up to 263 - 1; an
adversary post-handshake could advertise that to OOM the server
before any body bytes arrive. This cap is enforced on the
declared length in the frame header (before reading bytes off the
wire) and triggers CLOSE with status code 1009 (MESSAGE_TOO_BIG)
when exceeded. Default: 67108864 (64 MiB) — large enough to
pass the Autobahn|Testsuite 9.x large-message cases while still
bounding per-connection memory use. Lower for stricter exposure
(e.g. 1048576 for 1 MiB matching the
python-websockets default).
BB_COMPRESSION_MIN_SIZE
Minimum response body size in bytes below which
:class:~blackbull.middleware.compression.Compression skips
compression entirely. Raising this threshold under load reduces CPU
pressure at the cost of slightly larger small responses.
Default: 100.
BB_COMPRESSION_EXECUTOR_THRESHOLD
Body size in bytes above which compression is offloaded to a thread-pool
executor so the event loop can continue processing other requests during
the (CPU-heavy) compress call. 0 always compresses on the event loop
(disables offloading). Default: 65536 (64 KiB).
BB_COMPRESSION_MAX_INFLIGHT
Maximum number of compression offloads allowed to be running
concurrently in the asyncio default thread pool. When at or above
this cap, additional eligible responses are served uncompressed**
rather than queued — bounded fall-back rather than unbounded queue
growth. Tied to executor size: setting this above Python's default
ThreadPoolExecutor max_workers provides no benefit. That
default is min(32, os.cpu_count() + 4) on Python ≤ 3.12 and
min(128, os.cpu_count() * 5) on Python ≥ 3.13.
0 disables backpressure (unbounded queue, pre-0.29 behaviour).
Default: os.cpu_count() * 2.
BB_BROTLI_QUALITY
Brotli quality level (0–11) for dynamic-response compression. The
brotli library's own default is 11 — designed for build-time/static
pre-compression and far too expensive on the request path. 4 matches
Google's and Cloudflare's dynamic-content recommendation; 5 matches
Apache mod_brotli's default; 6 matches nginx ngx_brotli's default; 11
is appropriate only for offline pre-compression of static siblings.
Default: 4.
BB_FRAME_YIELD_EVERY
Number of stream tasks spawned per connection before the frame loop
inserts await asyncio.sleep(0) to let the event loop dispatch the
queued tasks. Under burst traffic (e.g. 500 VUs all sending at once)
the frame loop can process many HEADERS frames without yielding, which
stalls all waiting tasks and inflates p99 latency. Yielding every N
spawns caps the maximum synchronous run to N × ~50 µs regardless of
burst size. 0 disables cooperative yielding (legacy behaviour).
Default: 8.
BB_UVLOOP
Install the uvloop event loop policy before each
asyncio.run() when the optional [speed] extra is installed.
Falls back to the standard asyncio loop with a warning if uvloop is
not importable. Default: false.
BB_DEADLINE_TICK_MS
Polling interval (milliseconds) for the per-process deadline scanner
that enforces connection timeouts (BB_HEADER_TIMEOUT,
BB_BODY_TIMEOUT, BB_WRITE_TIMEOUT, BB_KEEP_ALIVE_TIMEOUT).
Smaller = tighter timeout granularity at a small CPU cost; larger =
more slack but cheaper. Default: 300.
Settings
dataclass
¶
Immutable snapshot of all runtime settings.
Construct via :func:get_settings rather than directly so that
environment variables are read at the right time.
apply_event_loop_policy(cfg=None)
¶
Install uvloop as the asyncio event loop policy if BB_UVLOOP=1.
Call this once before each asyncio.run() entry point. Safe to call
multiple times (subsequent calls are no-ops when the policy is already set).
If uvloop is not installed a warning is logged and the standard policy is
kept; the server still starts.
get_settings()
cached
¶
Read environment variables and return an immutable :class:Settings.
Cached: first call parses env vars and builds the dataclass; subsequent
calls return the same instance. Settings are server-process-wide
configuration, not per-request data — there's no reason to re-parse
os.environ on every request. Profile showed _int_env and
_int_env_nonneg consuming ~5–6% of CPU in the HTTP/1.1 hot path
before this cache.
Tests that mutate environment between cases must call
:func:reset_settings_cache in their teardown.
reset_settings_cache()
¶
Clear the cached :class:Settings.
Call this in test teardown if the test mutated env vars that
:func:get_settings reads. Without this, the cached settings reflect
whatever environment was visible the first time get_settings() ran
in the process.