Skip to content

Optimization Profiles ​

Prerequisites: Defining Pipelines, Pipeline Context

PipelineOptimizationProfile is a single toggle that controls how aggressively NPipeline optimizes for throughput vs. developer convenience. It affects runtime defaults, context dictionary thread safety, and which build-time analyzers are active.

The Two Profiles ​

DefaultHighThroughput
Target use casePrototyping, low-to-medium throughput, developer velocityProduction pipelines processing millions of items/second
Retry behaviorTransient item failures retried 3 times, with exponential backoff and jitterNo retries unless explicitly configured
Context dictionariesConcurrentDictionary (thread-safe)Dictionary (zero locking overhead)
Performance analyzersNP9103–NP9107 suppressedAll analyzers active
Memory modelSlightly higher per-context allocation (concurrent collections)Pooled dictionaries, minimal GC pressure
Configuration styleBatteries-included - works with minimal setupExplicit-everything - you configure what you need

Setting the Profile ​

At Runtime (PipelineBuilder) ​

csharp
public class MyPipeline : IPipelineDefinition
{
    public void Define(PipelineBuilder builder, PipelineContext context)
    {
        builder.WithOptimizationProfile(PipelineOptimizationProfile.HighThroughput);

        // ... add nodes and connections ...
    }
}

At Build Time (MSBuild) ​

The MSBuild property controls which analyzers fire during dotnet build:

xml
<PropertyGroup>
    <NPipelineOptimizationProfile>HighThroughput</NPipelineOptimizationProfile>
</PropertyGroup>

These two settings represent the same decision from different angles. Set them to the same value - the runtime profile governs execution behavior, while the MSBuild property governs analyzer behavior.

When profile metadata is available (the NPipeline package stamps it into your assembly at build time), NPipeline emits a runtime build warning if these values differ. This catches analyzer/runtime drift early.

Default Profile ​

The Default profile is designed for the 90% case: developers who want a working pipeline with sensible error recovery and don't need to micro-optimize memory allocations.

Automatic Retry Configuration ​

Unless you configure otherwise, the Default profile retries transient item failures (ItemRetryOptions.Default):

SettingValueEffect
ItemRetry.MaxRetries3A failed item is attempted up to 4 times in all
ItemRetry.BackoffExponential, full jitter200 ms base, 2× multiplier, 30 s cap
ItemRetry.ClassifierRetryClassifier.DefaultOnly transient failures are retried: timeouts, I/O and socket errors, HTTP 408/429/5xx, transient database errors, and client-side cancellations. A programming error fails at once
NodeRestart, NodeRetryOffConfigure them per pipeline or per node
OnItemFailureFailAn item that is not retried fails the node

No policy or configuration is needed for this:

csharp
public void Define(PipelineBuilder builder, PipelineContext context)
{
    // A TimeoutException thrown by ProcessOrder is retried up to 3 times with backoff.
    var source = builder.AddSource<OrderSource, Order>("orders");
    var transform = builder.AddTransform<ProcessOrder, Order, Result>("process");
    var sink = builder.AddSink<ResultSink, Result>("save");

    builder.Connect(source, transform);
    builder.Connect(transform, sink);
}

Override any setting without losing the rest. WithResilience starts from the profile's options:

csharp
builder.WithResilience(options => options with
{
    ItemRetry = options.ItemRetry with { MaxRetries = 5 },  // backoff and classifier remain
});

Thread-Safe Context Dictionaries ​

In the Default profile, PipelineContext.Parameters, .Items, and .Properties are backed by ConcurrentDictionary<string, object>. This means:

  • Concurrent reads and writes from parallel nodes do not throw or corrupt data.
  • No need to add explicit locking for simple shared counters or flags.
  • Safe to use with NPipeline.Extensions.Parallelism without additional synchronization for basic scenarios.

When you supply your own dictionaries through PipelineContextConfiguration, Parameters is copied into a ConcurrentDictionary, and Items and Properties are wrapped in a lock-based view so you keep seeing the pipeline's writes. The wrapper locks on the dictionary you supplied, so if your own code reads or writes that dictionary while a run is in progress, lock on the same instance.

csharp
// Safe in Default profile - ConcurrentDictionary handles concurrent writes
public override ValueTask<Order> TransformAsync(
    Order item, PipelineContext context, CancellationToken ct)
{
    context.Items["lastProcessed"] = item.Id;  // thread-safe write
    return ValueTask.FromResult(item);
}

Note: Thread-safe dictionaries prevent crashes and data corruption, but they do not prevent logical race conditions. For complex shared state with checkpoint/restore semantics, use IPipelineStateManager.

Suppressed Performance Analyzers ​

The following analyzers are inactive in the Default profile because their rules target micro-optimizations that only matter at extreme throughput:

RuleTitleWhy Suppressed
NP9103LINQ in hot pathsLINQ allocations are negligible below millions of items/sec
NP9104Inefficient string operationsString concatenation overhead is irrelevant at moderate scale
NP9105Anonymous object allocationObject allocation cost is insignificant for most workloads
NP9107Source node streamingMaterializing moderate datasets is acceptable for convenience

All other analyzers (configuration, reliability, data integrity, design) remain active regardless of profile.

HighThroughput Profile ​

The HighThroughput profile is for pipelines where every allocation counts - processing millions of items per second, sub-millisecond per-item latency targets, or GC-sensitive environments.

Explicit Configuration Required ​

Nothing is retried (PipelineResilienceOptions.None). Configure what you need explicitly:

csharp
public void Define(PipelineBuilder builder, PipelineContext context)
{
    builder.WithOptimizationProfile(PipelineOptimizationProfile.HighThroughput);

    builder.WithResilience(options => options with
    {
        ItemRetry = new ItemRetryOptions
        {
            MaxRetries = 3,
            Backoff = RetryBackoff.Exponential(TimeSpan.FromMilliseconds(100), maxDelay: TimeSpan.FromSeconds(10)),
        },
    });

    // ... add nodes and connections ...
}

To use the Default profile's item retry while staying in HighThroughput mode:

csharp
builder.WithOptimizationProfile(PipelineOptimizationProfile.HighThroughput);
builder.WithResilience(options => options with { ItemRetry = ItemRetryOptions.Default });

Zero-Overhead Context Dictionaries ​

Context dictionaries use pooled Dictionary<string, object> instances - no locking, no memory barriers, no ConcurrentDictionary overhead:

  • Dictionaries are rented from an object pool and returned on disposal.
  • Zero per-access synchronization cost.
  • Not thread-safe. Concurrent writes from parallel nodes cause data corruption.

When using parallel execution with HighThroughput, manage shared state through IPipelineStateManager or avoid writing to context dictionaries from transform nodes entirely.

All Performance Analyzers Active ​

All NP9103–NP9107 analyzers fire at build time, catching:

  • LINQ allocations in per-item transform methods
  • String concatenation in loops
  • Anonymous object allocation in hot paths
  • Opportunities to use ValueTask fast paths
  • Source nodes that materialize instead of streaming

This produces a stricter build experience that surfaces every potential allocation in the processing hot path.

Memory and Performance Characteristics ​

AspectImpact
Context dictionary access~0 overhead (no locks, no memory barriers)
Dictionary lifecyclePooled - no GC pressure from context creation/disposal
Per-item allocationsAnalyzer-enforced: flagged at build time
Retry configurationNo retries, buffering, or materialization unless explicitly configured

Per-Node Resilience ​

Give one node different options with WithResilience(handle, ...). The node's options derive from the pipeline's, so it keeps everything it does not change:

csharp
var transform = builder.AddTransform<CallApi, Request, Response>("api-call");

builder.WithResilience(transform, options => options with
{
    ItemRetry = options.ItemRetry with { MaxRetries = 10 },
});

Item retry and node restart apply only to transform nodes. Configuring these for a source, sink, or aggregate causes a build error. Use NodeRetry to execute a whole node again.

Choosing a Profile ​

mermaid
flowchart TD
    A[How many items per second?] --> B{"< 100K/sec"}
    A --> C{"> 100K/sec"}
    B --> D[Default]
    C --> E{GC-sensitive?}
    E --> F[Yes] --> G[HighThroughput]
    E --> H[No] --> D
ScenarioRecommended Profile
Prototyping and developmentDefault
ETL jobs processing thousands to hundreds of thousands of recordsDefault
Real-time event processing at millions of items/secHighThroughput
Latency-sensitive financial data pipelinesHighThroughput
Batch jobs where simplicity matters more than throughputDefault
Microservice integrations with moderate loadDefault

Interaction with Other Configuration ​

Explicit Resilience Options Build on the Profile ​

WithResilience() receives the profile's options and returns yours, so it changes only what you set. It runs when the pipeline is built, so it can come before or after WithOptimizationProfile():

csharp
builder.WithResilience(options => options with { ItemRetry = options.ItemRetry with { MaxRetries = 10 } });
builder.WithOptimizationProfile(PipelineOptimizationProfile.Default);
// Result: 10 retries of transient failures, with the Default profile's backoff and classifier

To opt out of the profile's retries entirely, return PipelineResilienceOptions.None.

.editorconfig Overrides ​

Individual analyzer rules can always be suppressed or promoted via .editorconfig, regardless of the MSBuild profile setting:

ini
[*.cs]
# Suppress NP9103 even in HighThroughput mode
dotnet_diagnostic.NP9103.severity = none

# Enable NP9107 even in Default mode
dotnet_diagnostic.NP9107.severity = warning

Parallelism Extension ​

The NPipeline.Extensions.Parallelism package works with both profiles. The key difference is context dictionary safety:

ProfileParallel Context AccessRecommendation
DefaultSafe (ConcurrentDictionary)Works out of the box for basic shared state
HighThroughputUnsafe (Dictionary)Use IPipelineStateManager or avoid context writes

Next Steps ​

Released under the MIT License.