# Sampling


Sending every trace your application produces gets expensive fast and rarely tells you anything new. Sampling lets you keep a representative slice of traffic and drop the rest. When a trace is dropped, no spans are collected or sent for it.

A sampled trace is not free. Recording spans, capturing query bindings, gathering request and response data, and serialising the trace before it is shipped all add overhead to the request, command, or job that produced it. We work hard to keep this overhead as small as possible, and unsampled traces stop almost all of the work before it starts, but on hot paths the difference between a sampled and an unsampled trace is still measurable. Picking a sample rate is therefore a trade-off between coverage and runtime cost.

## Setting a sample rate

The simplest setup is a single rate that applies to every trace. The default is 10%. Change it with `sampleRate()`:

```php
$config->sampleRate(0.5); // 50% of all traces will be sent to Flare
$config->sampleRate(1.0); // 100%
```

For full-on or full-off, use the convenience helpers:

```php
$config->alwaysSampleTraces();
$config->neverSampleTraces();
```

## Sampling per kind of work

When you want more control than a single uniform rate, Flare can apply different rates to different kinds of work. Health checks can be sampled rarely, checkout flows aggressively, and noisy queue jobs can follow their own logic. Configure it with `sampleTracesDynamic()` and a list of rules:

```php
use Spatie\FlareClient\Sampling\SamplingRule;

$config->sampleTracesDynamic(0.1, [
    SamplingRule::forPath('/health-check', 0.0),
    SamplingRule::forPath('/admin/*', 1.0),
    SamplingRule::forRoute('checkout.*', 1.0),
    SamplingRule::forCommand('horizon:work', 0.0),
    SamplingRule::forJob('App\\Jobs\\ProcessPayment', 1.0),
]);
```

The first argument is the base rate, used when no rule matches. Rules are evaluated in order, and the first one that matches wins.

### Rule types

| Method | Matches against | When the entry point type is |
|---|---|---|
| `SamplingRule::forUrl($pattern, $rate)` | The full URL (including scheme and host) | `web` |
| `SamplingRule::forPath($pattern, $rate)` | The URL path | `web` |
| `SamplingRule::forRoute($pattern, $rate)` | The matched route pattern (without HTTP method prefix) | `web` |
| `SamplingRule::forCommand($pattern, $rate)` | The command name | `cli` |
| `SamplingRule::forJob($pattern, $rate)` | The job name | `queue` |
| `SamplingRule::using($closure)` | Anything you want, evaluated when the trace starts | All |
| `SamplingRule::usingDeferred($closure)` | Anything you want, evaluated after the handler is resolved | All |

`forRoute` waits for routing middleware to match a route. `forCommand` waits for the command class to be resolved. `usingDeferred` waits for the same handler resolution before invoking its closure. Every other rule type runs the moment the request, job, or command starts. Flare aims to make the sampling decision as early as possible. See [Deferred sampling](#deferred-sampling) for details.

### Pattern syntax

Patterns are literal strings with `*` as a wildcard. Any other character is matched literally. There is no support for `?`, character classes, or full regex.

```php
SamplingRule::forPath('/api/*', 0.1);          // matches /api/users, /api/orders/123
SamplingRule::forJob('App\\Jobs\\*Email', 1.0); // matches SendWelcomeEmail, SendInvoiceEmail
SamplingRule::forCommand('horizon:*', 0.0);    // matches horizon:work, horizon:status
```

### Closure rules

When a pattern is not expressive enough, use a closure:

```php
SamplingRule::usingDeferred(function (EntryPoint $entryPoint) {
    if ($entryPoint->handlerName === LongRunningReportJob::class) {
        return 1.0;
    }

    return null; // No opinion: fall through to the next rule
});
```

The closure receives the [`EntryPoint`](/docs/protocol/general/entry-points) and returns either a rate (`0.0` to `1.0`) or `null` to defer the decision to the next rule.

`SamplingRule::using()` is evaluated when the request, job, or command starts, so the entry point will not have a resolved handler yet. Use `SamplingRule::usingDeferred()` when you need to decide based on the route, command, or job class, and want to wait for the framework to resolve it.

## Deferred sampling

Some rules cannot run when the trace starts. A web framework only knows the matched route after routing middleware runs, and a queue worker may only know the job class after it pulls the payload off the queue.

When a rule needs information that has not been resolved yet, the sampler defers its decision. The trace is sampled tentatively (so spans are still collected), and the sampler is re-evaluated as soon as the framework resolves the handler. The recorders trigger that re-evaluation automatically:

- The [routing recorder](/docs/php/data-collection/routing) calls re-evaluation in `recordRoutingEnd`, so `forRoute` rules can fire as soon as the route is known.
- The command recorder re-evaluates in `recordStart`, once the command class is set.
- The job recorder re-evaluates in `recordStart` (subtask mode only), once the job class is set.

If the sampler decides to drop the trace at re-evaluation time, the spans collected so far are discarded.

## Custom rules

For reusable logic, write your own rule class. Extend `SamplingRule`, declare which entry-point types the rule applies to, and return a rate from `getMatchedRate`. Add the `DeferredSamplerRule` marker if your rule depends on the resolved handler.

```php
use Spatie\FlareClient\Enums\EntryPointType;
use Spatie\FlareClient\EntryPoint\EntryPoint;
use Spatie\FlareClient\Sampling\DeferredSamplerRule;
use Spatie\FlareClient\Sampling\SamplingRule;

class AdminControllerRule extends SamplingRule implements DeferredSamplerRule
{
    public function __construct(
        protected string $pattern,
        protected float $rate,
    ) {
    }

    public function appliesTo(EntryPointType $entryPointType): bool
    {
        return $entryPointType === EntryPointType::Web;
    }

    public function getMatchedRate(EntryPoint $entryPoint): ?float
    {
        return str_starts_with($entryPoint->handlerName ?? '', $this->pattern)
            ? $this->rate
            : null;
    }
}
```

Pass instances to `sampleTracesDynamic()` alongside the built-in rules:

```php
$config->sampleTracesDynamic(0.1, [
    new AdminControllerRule('App\\Http\\Controllers\\Admin\\', 1.0),
    SamplingRule::forPath('/health-check', 0.0),
]);
```

## Continuing traces from upstream services

When a request reaches your application with a W3C `traceparent` header (or a job carries one in its payload), Flare extracts the traceId and parent span id so the trace stays connected to the upstream service. The sampler still decides whether the trace is recorded, and it now receives the upstream's sampling decision as a hint.

Each built-in sampler handles that hint differently:

- `AlwaysSampler` and `NeverSampler` ignore it (their name is the contract).
- `RateSampler` honors the upstream decision when present, otherwise it applies the configured rate. This keeps a continued trace consistent with its parent.
- `DynamicSampler` evaluates its rules first. A matching rule wins, even when the upstream said otherwise. If no rule matches, the upstream decision is honored, falling back to `base_rate` only when there is no parent.

This means a rule like `SamplingRule::forJob(ProcessPayment::class, 1.0)` will sample the job's trace regardless of whether the dispatching request was sampled. The trade-off is that a rule overriding the upstream decision produces a partial trace from the upstream perspective: the parent service has spans that no longer connect to a child trace, or the other way around.

## Custom samplers

You can replace the sampler entirely. Implement the `Sampler` interface:

```php
use Spatie\FlareClient\EntryPoint\EntryPoint;
use Spatie\FlareClient\Sampling\Sampler;

class AlwaysSampler implements Sampler
{
    public function __construct(protected array $config) {}

    public function shouldSample(EntryPoint $entryPoint, ?bool $parentSampled = null): bool
    {
        return true;
    }
}
```

`$parentSampled` carries the upstream sampling decision when the trace was started from a `traceparent`, or `null` when there is no parent. Use it to keep continued traces consistent with their parent, or ignore it to keep your sampler's behavior independent of upstream services.

Register the sampler with `sampler()`. The optional config array is passed to the constructor:

```php
$config->sampler(AlwaysSampler::class, ['some_option' => true]);
```

If your sampler should support deferred sampling (so it can change its mind once a handler is resolved), implement `DeferrableSampler` instead. It adds three methods: `isDeferred()` to report whether the sampler is waiting for more information, `reevaluate(EntryPoint $entryPoint)` to make a final decision, and `reset()` to clear the deferred state. The `DynamicSampler` documented above is the reference implementation.
