Summary
Langfuse(sample_rate=0.0) traces everything. _client/client.py:354 is
sample_rate = sample_rate or float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0))
and 0.0 or 1.0 is 1.0, so the value is replaced before it is used. The next line validates
0.0 <= sample_rate <= 1.0 and the docstring documents the range as "0.0 to 1.0", so 0.0 is an
accepted, documented value that never reaches the sampler.
Reproduction
langfuse 4.14.1. 40 observations per case, counting spans that arrive at a SpanExporter.
| how the rate was set |
stored sample_rate |
sampler |
spans exported |
sample_rate=0.0 keyword |
1.0 |
ParentBased (the default) |
40 of 40 |
sample_rate=0.5 keyword |
0.5 |
TraceIdRatioBased |
14 of 40 |
| nothing passed |
1.0 |
ParentBased (the default) |
40 of 40 |
LANGFUSE_SAMPLE_RATE=0.0 |
0.0 |
TraceIdRatioBased |
0 of 40 |
The keyword and the environment variable carry the same value and do opposite things. The
keyword path is also the one that does the opposite of what was asked for, which is the part
that would cost someone money.
Two things had to be true for those numbers to mean anything, and both caught me out first:
langfuse keeps one resource manager per public_key, and an OpenTelemetry global tracer
provider can only be registered once per process. Sharing either across cases silently reuses
the first client's pipeline. Each row above is a separate process building exactly one client.
Suggested fix
if sample_rate is None:
sample_rate = float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0))
timeout seven lines down reads the same way (timeout or int(os.environ.get(LANGFUSE_TIMEOUT, 5))), so a keyword 0 is unreachable there too. I do not know whether 0 means anything for a
timeout in your client, so I am pointing at the shape and not calling it a second bug.
flush_at, flush_interval and media_upload_thread_count are fine, they pass through as
Optional and are stored as given.
A separate one, if you want it
Three code examples in the shipped docstrings call a method that no longer exists. client.py:289,
which is the Langfuse class docstring and so the first thing help(Langfuse) prints, has
with span.start_as_current_generation(name="generate-response", model="gpt-4") as generation:
and client.py:1446 plus client.py:2147 have langfuse.start_as_current_generation(...). Run
verbatim on 4.14.1 those raise
AttributeError: 'LangfuseSpan' object has no attribute 'start_as_current_generation'
AttributeError: 'Langfuse' object has no attribute 'start_as_current_generation'
start_as_current_observation(name=..., as_type="generation") is the working form. Say the word
and I will open that separately instead of filing two at once.
How I found this, and what I am not claiming
I maintain a test harness that measures whether coding models can drive a given SDK, by running
the code they write and asserting on the HTTP the SDK emits. This is not that. langfuse has
never been through it, so I have no numbers about models and this is not a report about them. I
went looking at how the client resolves configuration and found the sampling line by reading,
then measured it.
toolshed is a small studio run by its owner, who directs the work, and AI does a lot of the
engineering.
Cal / toolshed / toolshedlabs@gmail.com
Summary
Langfuse(sample_rate=0.0)traces everything._client/client.py:354isand
0.0 or 1.0is1.0, so the value is replaced before it is used. The next line validates0.0 <= sample_rate <= 1.0and the docstring documents the range as "0.0 to 1.0", so 0.0 is anaccepted, documented value that never reaches the sampler.
Reproduction
langfuse 4.14.1. 40 observations per case, counting spans that arrive at a
SpanExporter.sample_ratesample_rate=0.0keywordsample_rate=0.5keywordLANGFUSE_SAMPLE_RATE=0.0The keyword and the environment variable carry the same value and do opposite things. The
keyword path is also the one that does the opposite of what was asked for, which is the part
that would cost someone money.
Two things had to be true for those numbers to mean anything, and both caught me out first:
langfuse keeps one resource manager per
public_key, and an OpenTelemetry global tracerprovider can only be registered once per process. Sharing either across cases silently reuses
the first client's pipeline. Each row above is a separate process building exactly one client.
Suggested fix
timeoutseven lines down reads the same way (timeout or int(os.environ.get(LANGFUSE_TIMEOUT, 5))), so a keyword 0 is unreachable there too. I do not know whether 0 means anything for atimeout in your client, so I am pointing at the shape and not calling it a second bug.
flush_at,flush_intervalandmedia_upload_thread_countare fine, they pass through asOptional and are stored as given.
A separate one, if you want it
Three code examples in the shipped docstrings call a method that no longer exists.
client.py:289,which is the
Langfuseclass docstring and so the first thinghelp(Langfuse)prints, hasand
client.py:1446plusclient.py:2147havelangfuse.start_as_current_generation(...). Runverbatim on 4.14.1 those raise
start_as_current_observation(name=..., as_type="generation")is the working form. Say the wordand I will open that separately instead of filing two at once.
How I found this, and what I am not claiming
I maintain a test harness that measures whether coding models can drive a given SDK, by running
the code they write and asserting on the HTTP the SDK emits. This is not that. langfuse has
never been through it, so I have no numbers about models and this is not a report about them. I
went looking at how the client resolves configuration and found the sampling line by reading,
then measured it.
toolshed is a small studio run by its owner, who directs the work, and AI does a lot of the
engineering.
Cal / toolshed / toolshedlabs@gmail.com