skip to content

What do Helicone-Property-* headers do to a logged request, and why set them?

level: middleimportance: should knowfreq 55%

answer

  1. headers become dashboard columns
  2. the name after the prefix is yours
  3. group by feature, tenant, environment
  4. only requests that carried it have a value
  5. unbounded values make useless groups

basics

~20 s

A header named Helicone-Property-<Name> attaches an arbitrary key/value tag to that request's log row. Those tags become filter and group-by dimensions in Helicone's dashboards, which is how you get cost and latency broken down per feature, environment or customer.

solid answer

~50 s

Custom properties are how raw request logs become answerable questions. You send `Helicone-Property-Environment: staging` or `Helicone-Property-Feature: summarizer` on the call, and Helicone stores those values alongside the model, tokens and computed cost. In the dashboard they turn into dimensions: spend by feature, latency by tenant, error rate by app version. The name after the prefix is arbitrary — you invent your vocabulary — and the value is a string. Two practical rules follow. First, only requests that carried the header have a value for it, so a property you add today does not exist on yesterday's traffic; roll it out before you need the breakdown. Second, keep values **low cardinality** and enumerable — environment, feature, tenant, prompt version. Putting a per-request unique value such as raw prompt text in a property gives you a dimension with one row per group, which no dashboard can aggregate; identity belongs in `Helicone-User-Id` and call grouping in the session headers.

code

python · 21 lines
python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="https://oai.helicone.ai/v1",
    default_headers={
        "Helicone-Auth": f"Bearer {os.environ['HELICONE_API_KEY']}",
        "Helicone-Property-Environment": os.environ.get("APP_ENV", "development"),
        "Helicone-Property-Service": "support-api",
    },
)

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Summarise this ticket."}],
    extra_headers={
        "Helicone-Property-Feature": "ticket-summary",
        "Helicone-User-Id": "acct_8814",
    },
)

go deeper

for a junior

Know the header shape — Helicone-Property-<Name>: <value> — and that it tags the log row so you can filter and group by it later.

for a middle

Explain why the values must be low cardinality and enumerable, and where stable properties belong (client defaults) versus per-call ones.

for a senior

Talk about rollout: properties are not retroactive, they must not carry PII or secrets, and normalising values in one place prevents the prod/production drift that ruins aggregation.

for a principal

Own the vocabulary as a fleet-wide contract enforced by a shared client, and connect it to the cost-attribution and chargeback questions the business will eventually ask.

## The problem properties solve A logging tool that records every LLM call gives you a firehose: thousands of rows, each with a model, a prompt, a latency and a cost. The question you actually have is never "what did request 84719 cost" — it is "which feature is burning the budget", "is staging traffic polluting my production numbers", "which tenant is responsible for the p99". Answering those requires the log row to carry the dimension you want to slice by, and only your application knows it. Custom properties are the channel for that knowledge. ## The mechanism Any request header whose name begins with `Helicone-Property-` is treated as a custom property. The remainder of the header name is the property name; the header value is the property value. ``` Helicone-Property-Environment: production Helicone-Property-Feature: ticket-summary Helicone-Property-AppVersion: 2026.8.3 ``` Helicone stores those with the request and exposes them as columns you can filter and group by. There is no schema to register in advance and no configuration step: the first request carrying a new property name creates the dimension. Because they are ordinary request headers, they compose naturally with how clients are constructed. Stable values — environment, service name, release version — belong in the client's default headers so every call carries them without thought. Call-specific values — which feature or workflow made this particular call — are set per request. ## Designing the vocabulary The mechanism is trivial; using it well is the skill. **Low cardinality.** A property is useful in proportion to how many requests share each value. `Feature: summarizer` with ten thousand rows behind it is a useful group. A property whose value is unique per request produces ten thousand groups of one and is unusable for aggregation, while bloating the dimension. Reserve identity for `Helicone-User-Id` and per-run grouping for the session headers, which exist precisely so you are not tempted to abuse a property for them. **Enumerable and stable.** If you cannot list the possible values of a property on a whiteboard, it is probably the wrong property. Free-form values drift — `prod`, `production` and `Production` become three groups that should have been one — so normalise them in one place rather than at every call site. **No secrets and no PII.** A property value is stored in the trace record and visible to anyone with dashboard access. API keys, tokens, email addresses and raw user content do not belong there. **Not retroactive.** Properties describe the request that carried them. Traffic logged before you introduced a property simply has no value for it, so comparisons across the rollout boundary are misleading. Add the dimension ahead of the investigation you expect to need it for. ## What you get back Once the vocabulary exists, the dashboards answer real questions without further work: - **Cost attribution.** Spend grouped by feature or tenant, which is the input to any chargeback or pricing conversation. - **Environment hygiene.** Filtering staging and evaluation traffic out of production numbers — worth doing on day one, since eval runs can dwarf real traffic. - **Change tracking.** Grouping by app or prompt version to see whether a release moved latency, token consumption or cost. - **Triage.** Narrowing an incident to the subset of traffic that shares a property value, instead of scrolling a global log. ## Mode independence Properties are fields on the log record, not an interception feature, so they work the same whether the request went through the proxy or was reported by async logging. In proxy mode they ride as request headers; in async logging they are part of the payload you send. That is what keeps a mixed fleet coherent: services on either integration land in the same project and can be sliced by the same dimensions, as long as they agree on the names. ## The usual mistakes Teams that get little value out of properties almost always made one of three mistakes: they let every service invent its own names, so nothing aggregates across the fleet; they put something unbounded in a value, so the dimension is meaningless; or they added the header only after an incident, and discovered the historical data they wanted does not exist. All three are avoidable by setting a small, agreed vocabulary in a shared client wrapper before you need it.

  • How do you decide between a custom property and Helicone-User-Id for a value?
    Use `Helicone-User-Id` for the identity of who the call was for — it is the dedicated identity dimension and feeds per-user views. Use a property for descriptive, low-cardinality context about the call: environment, feature, tenant tier, prompt version. The test is cardinality and intent: one value per person is identity, one value per category is a property.
  • You add a new property today. Can you break down last month's spend by it?
    No. Properties are recorded on the request that carried the header, so traffic logged before the rollout has no value for the new dimension and will group as missing. The practical response is to roll out the header first and accept a gap, or to compare only periods after the rollout. It is a good argument for putting a small standard set of properties on every client from the start.
  • Do custom properties work with async logging as well as the proxy?
    Yes. They are fields on the log record rather than something the proxy has to intercept, so a service using async logging can carry the same property names as a proxied service, and both are filterable together in the same project. That is what makes running mixed integration modes across a fleet practical.

saying these in an interview costs you the question

  • Putting raw prompt text or user emails in a property value
  • Expecting properties to backfill onto older requests
  • Inventing per-service property names with no shared vocabulary
  • Thinking properties require the proxy integration
  • Using a per-request unique value as a property

context