Skip to main content
This guide answers a common production question: where should you initialize the tracer? The answer depends on your runtime. The tracer placement is different for a script, evaluate(), Lambda, and a long-running web server because session state is handled differently in each pattern.

Which Pattern?

RuntimeWhere to initializeSession strategyWhy
Scripts / notebooksModule-level in the entry pointOne shared session is often enoughSimple single execution flow
AWS Lambda / Cloud FunctionsOutside the handler with lazy initcreate_session() per invocationReuse warm containers without sharing invocation state
FastAPI / Flask / DjangoOnce at app startupcreate_session() or acreate_session() per requestReuse one tracer while isolating concurrent requests
Initialize the tracer before any instrumentor. Call HoneyHiveTracer.init(...) first, then pass tracer.provider into instrumentor.instrument(...).What changes by runtime is not whether you initialize the tracer. What changes is where you place that initialization and how you create session context.For request-scoped and invocation-scoped runtimes, create_session() and acreate_session() put the active session ID in OpenTelemetry baggage. HoneyHive resolves that baggage session before falling back to the tracer instance’s startup session.
evaluate() is the main exception to the table above. When you’re running experiments with evaluate(), do not initialize your own tracer. The SDK creates and manages a separate tracer for each datapoint.

Scripts and Notebooks

Initialize once at module level. All traced operations share the same session.
from honeyhive import HoneyHiveTracer, trace
import os

tracer = HoneyHiveTracer.init(
    api_key=os.getenv("HH_API_KEY"),
    project="my-project",
    session_name="local-dev-session"
)

@trace(event_type="tool", tracer=tracer)
def process_data(input_text):
    result = transform(input_text)
    tracer.enrich_span(metadata={"input_length": len(input_text)})
    return result

if __name__ == "__main__":
    result1 = process_data("Hello")
    result2 = process_data("World")
This is the simplest pattern. Use it for scripts, notebooks, and quick debugging.

Evaluation / Experiments

When running experiments with evaluate(), don’t create your own tracer. The SDK creates a new tracer per datapoint automatically, giving each datapoint its own isolated session.
from honeyhive import trace
from honeyhive.experiments import evaluate
import os

# No HoneyHiveTracer.init() here

@trace(event_type="tool")  # No tracer parameter
def my_rag_pipeline(datapoint: dict):
    inputs = datapoint["inputs"]
    response = generate_response(inputs["query"], inputs["context"])
    return {"answer": response}

result = evaluate(
    function=my_rag_pipeline,
    dataset=my_dataset,
    api_key=os.getenv("HH_API_KEY"),
    project="my-project",
    name="rag-experiment-1"
)
Don’t initialize a global tracer alongside evaluate().A global tracer can conflict with the per-datapoint tracers that evaluate() creates. If you see traces landing in the wrong session, remove the global HoneyHiveTracer.init() call.
# Wrong -- global tracer conflicts with evaluate()
tracer = HoneyHiveTracer.init(...)

@trace(event_type="tool", tracer=tracer)  # Forces all datapoints to share one session
def my_function(input):
    pass

# Correct -- let evaluate() manage tracers
@trace(event_type="tool")  # evaluate() provides isolated tracer per datapoint
def my_function(input):
    pass

Serverless

In serverless environments like Lambda and Cloud Functions, initialize the tracer outside the handler and reuse it across warm starts. Then call create_session() inside the handler so each invocation gets its own active session. The invocation-scoped baggage session takes precedence over any default session on the shared tracer.
from honeyhive import HoneyHiveTracer, trace
import os
from typing import Optional

_tracer: Optional[HoneyHiveTracer] = None  # Survives warm starts

def get_tracer() -> HoneyHiveTracer:
    global _tracer
    if _tracer is None:
        _tracer = HoneyHiveTracer.init(
            api_key=os.getenv("HH_API_KEY"),
            project=os.getenv("HH_PROJECT"),
            source="lambda"
        )
    return _tracer

def lambda_handler(event, context):
    tracer = get_tracer()

    # Create a new session for this invocation
    session_id = tracer.create_session(
        session_name=f"lambda-{context.aws_request_id}",
        inputs={"event": event}
    )

    result = process_event(event)

    tracer.enrich_session(
        outputs={"result": result},
        metadata={"request_id": context.aws_request_id}
    )
    tracer.force_flush(timeout_millis=5000)
    return result

@trace(event_type="tool")
def process_event(event):
    get_tracer().enrich_span(metadata={"event_type": event.get("type")})
    return {"status": "success"}
If your function returns immediately after handling the request, call tracer.force_flush() before returning so buffered spans are exported before the runtime freezes.
LRU cache alternative for lazy initialization:
from functools import lru_cache

@lru_cache(maxsize=1)
def get_tracer():
    return HoneyHiveTracer.init(
        api_key=os.getenv("HH_API_KEY"),
        project=os.getenv("HH_PROJECT")
    )

Linking Lambda Invocations

To link multiple invocations into the same session (e.g., multi-turn conversations), pass a session_id through your event payload and reuse get_tracer() from the lazy-init pattern above:
import uuid

def lambda_handler(event, context):
    tracer = get_tracer()
    existing_session_id = event.get("session_id")

    if existing_session_id:
        # Link to an existing session (no API call)
        tracer.create_session(session_id=existing_session_id, skip_api_call=True)
        session_id = existing_session_id
    else:
        # Create a new session with your own ID
        session_id = tracer.create_session(
            session_id=str(uuid.uuid4()),
            session_name=f"lambda-{context.function_name}",
            inputs={"event": event}
        )

    result = process_event(event)
    tracer.enrich_session(outputs={"result": result})
    return {"session_id": session_id, "result": result}

Web Servers

For long-running servers (FastAPI, Flask, Django), initialize one tracer at startup and create a new session per request using create_session() or its async variant acreate_session().
How session isolation works: create_session() and acreate_session() store the active session ID in OpenTelemetry baggage, which uses Python context propagation and ContextVar for async/task-local state. HoneyHive reads the baggage session first and only falls back to the tracer instance when no request-scoped session is present, so one shared tracer can safely serve concurrent requests.

FastAPI

from fastapi import FastAPI, Request
from honeyhive import HoneyHiveTracer, trace
import os

tracer = HoneyHiveTracer.init(
    api_key=os.getenv("HH_API_KEY"),
    project="my-api",
    source="production"
)

app = FastAPI()

@app.middleware("http")
async def session_middleware(request: Request, call_next):
    session_id = await tracer.acreate_session(
        session_name=f"api-{request.url.path}",
        inputs={
            "method": request.method,
            "path": str(request.url),
            "user_id": request.headers.get("X-User-ID")
        }
    )

    response = await call_next(request)

    tracer.enrich_session(outputs={"status_code": response.status_code})

    if session_id:
        response.headers["X-Session-ID"] = session_id

    return response

@app.post("/api/chat")
@trace(event_type="chain", tracer=tracer)
async def chat_endpoint(message: str):
    tracer.enrich_span(metadata={"message_length": len(message)})
    response = await process_message(message)
    return {"response": response}

@trace(event_type="tool", tracer=tracer)
async def process_message(message: str):
    result = await llm_call(message)
    return result

Flask

For synchronous frameworks, use create_session() instead of acreate_session():
from flask import Flask, request
from honeyhive import HoneyHiveTracer, trace
import os

tracer = HoneyHiveTracer.init(
    api_key=os.getenv("HH_API_KEY"),
    project="my-flask-app",
    source="production"
)

app = Flask(__name__)

@app.before_request
def create_session_for_request():
    tracer.create_session(
        session_name=f"flask-{request.path}",
        inputs={"method": request.method}
    )

@app.after_request
def enrich_session_after_request(response):
    tracer.enrich_session(outputs={"status_code": response.status_code})
    return response

@app.route("/api/process", methods=["POST"])
@trace(event_type="tool", tracer=tracer)
def process_endpoint():
    return {"result": "ok"}
Don’t use session_start() for web servers. session_start() stores the session ID on the tracer instance itself, which causes race conditions when multiple requests run concurrently. Use create_session() or acreate_session() instead. They store the session ID in request-scoped baggage.

Multi-Turn Conversations

For multi-turn conversations, the first request creates a session and returns the ID to the client. Subsequent requests link to that session using skip_api_call=True, which sets the session context without making an API call.
@app.middleware("http")
async def session_middleware(request: Request, call_next):
    existing_session = request.headers.get("X-Session-ID")

    if existing_session:
        # Link to existing session (no API call)
        await tracer.acreate_session(
            session_id=existing_session,
            skip_api_call=True
        )
    else:
        # Create new session
        session_id = await tracer.acreate_session(
            session_name=f"conversation-{request.url.path}"
        )
        request.state.new_session_id = session_id

    response = await call_next(request)

    if hasattr(request.state, "new_session_id"):
        response.headers["X-Session-ID"] = request.state.new_session_id

    return response
ScenarioCodeWhen
Auto-generate IDcreate_session(session_name="request")New session, let HoneyHive assign the ID
Custom IDcreate_session(session_id="my-id")Use your own ID scheme
Link to existingcreate_session(session_id="existing-id", skip_api_call=True)Session already exists in HoneyHive

Scoped Sessions

For single-use scripts, dedicated worker runs, or batch tasks where the rest of the current execution context belongs to the same logical unit of work, with_session can be convenient. For web requests, prefer create_session() or acreate_session() in middleware:
with tracer.with_session("batch-job", inputs={"batch_id": batch_id}) as session_id:
    process_batch(items)
    tracer.enrich_session(outputs={"processed": len(items)})

Thread and Process Safety

The global tracer + create_session() pattern is safe for:
  • Multi-threaded servers (FastAPI, Flask with threads) — baggage uses ContextVar, which is inherently thread-local
  • Multi-process deployments (Gunicorn workers, uWSGI) — each process gets its own tracer instance; processes don’t share state

Best Practices

Passing tracer=tracer makes the binding explicit and avoids relying on implicit tracer discovery.
tracer = HoneyHiveTracer.init(...)

@trace(event_type="tool", tracer=tracer)  # Explicit
def my_function():
    tracer.enrich_span(...)
Even with a global tracer, create sessions to isolate traces by request, user, or job.
# Per user request
session_id = tracer.create_session(session_name=f"user-{user_id}")

# Per batch job
session_id = tracer.create_session(session_name=f"batch-{batch_id}")
Use the tracer placement that matches your runtime:
  • Scripts and notebooks: initialize once in the module that starts the run
  • Lambda and other serverless runtimes: lazy-init outside the handler, then create a session per invocation
  • Web servers: initialize once at startup, then create a session per request
  • evaluate(): let the SDK create and manage tracers for you
test_mode=True disables OTLP export and generates a local session ID instead of creating one in HoneyHive. Use it for local development and tests when you want tracer setup without exporting spans over OTLP.
tracer = HoneyHiveTracer.init(
    api_key=os.getenv("HH_API_KEY"),
    project="my-project",
    test_mode=True
)