import coolhand auto-initializes monitoring and patches httpx.Client.send/httpx.AsyncClient.send and requests.Session.send in whichever Python interpreter runs that import. That single sentence is also the source of every pitfall below: the patch lives in one interpreter's memory, so anything that changes which interpreter is actually making your AI calls — worker processes, forking strategy, or another library patching the same functions — can put you back on unpatched code without any error.
Re-import Coolhand in every worker process, not just your entrypoint
Why it matters: Background job frameworks (Dramatiq, Celery, RQ) and process-based servers (Gunicorn with process workers) commonly start worker processes with a fresh Python interpreter — for example, invoking python -m dramatiq myapp. A worker process started this way does not inherit the import coolhand patch from your main application process; it begins completely unpatched, because it's a different interpreter, not a different thread.
# ❌ Broken — coolhand is imported in main.py, but dramatiq workers
# start a fresh interpreter and never import main.py at all
# main.py
import coolhand
from myapp.actors import send_notification # workers never load this path
# myapp/actors.py
import dramatiq
@dramatiq.actor
def send_notification(...):
... # AI calls here are never intercepted
# ✅ Fixed — import coolhand at the top of the module dramatiq actually
# re-imports inside each worker process
# myapp/actors.py
import coolhand # runs in every worker process, since dramatiq imports this module directly
import dramatiq
@dramatiq.actor
def send_notification(...):
... # ✅ intercepted
Coolhand also reads COOLHAND_API_KEY from the environment at import time — make sure that variable is actually set in the worker process's environment, not just the parent process that spawns it.
Know whether your workers fork() or spawn()
Why it matters: Coolhand's patch survives fork() (the child process inherits the parent's already-patched memory) but not spawn() (a fresh interpreter, same as the process-worker case above). This matters when choosing a multiprocessing start method or a Gunicorn worker class — a worker model that spawns rather than forks needs the same "import coolhand in the worker module" fix as above, even if the app "looks" like a single codebase.
Test your actual patch ordering, not just the happy path
Why it matters: If your app also uses other instrumentation that patches the same HTTP libraries — most commonly OpenTelemetry's httpx/requests auto-instrumentation — the order those patches get applied in can determine whether Coolhand's capture actually fires, or whether it gets wrapped/bypassed by the other library's patch. Don't assume the common case (Coolhand initialized first) is the only case your app will hit in production; explicitly test the order you're least confident about.
Pass silent=True if you use structured logging
Why it matters: By default, Coolhand's setup can call logging.basicConfig, which is fine for apps using the standard logging module but will clobber configuration for apps using structured logging (e.g. structlog). If your app configures its own logging pipeline, initialize Coolhand with silent=True (or set COOLHAND_SILENT=true) so it doesn't touch global logging config:
# ✅ Don't let Coolhand's default logging setup override structlog's configuration
from coolhand import Coolhand
Coolhand(api_key=..., silent=True)
Verify interception in CI, not just a 200 response
An example app or integration test returning a correct response proves your AI provider integration works — it does not prove Coolhand captured it. All three process-model pitfalls above produce a fully working app with silently missing logs. Assert programmatically that a request was actually intercepted (e.g. check httpx_interceptor.is_patched() and that a mocked call was actually captured) rather than relying on the app returning success.