Skip to content

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.