ConcurrentBag
C#
multithreading
concurrency
data structures

What is the correct usage of ConcurrentBag?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

ConcurrentBag is a thread-safe unordered collection in .NET designed for fast producer and consumer scenarios where ordering does not matter. It is useful, but often misapplied as a drop-in replacement for List or Queue. Correct usage starts with understanding its semantics: thread safety and high throughput, but no predictable item order.

Core Sections

What ConcurrentBag is for

ConcurrentBag works best when many threads add and remove items independently and you do not need FIFO or LIFO guarantees. It is optimized for local thread work stealing behavior, which can reduce contention in parallel workloads.

csharp
1using System.Collections.Concurrent;
2using System.Threading.Tasks;
3
4var bag = new ConcurrentBag<int>();
5
6Parallel.For(0, 1000, i => bag.Add(i));
7
8Console.WriteLine($"Count: {bag.Count}");

Use it for task result aggregation, temporary work buffers, and unordered accumulation.

Typical read and remove patterns

Use TryTake for destructive reads and TryPeek for non-destructive reads. Never assume items come back in insertion order.

csharp
1if (bag.TryTake(out var item))
2{
3    Console.WriteLine($"Took item {item}");
4}
5
6if (bag.TryPeek(out var top))
7{
8    Console.WriteLine($"Peeked item {top}");
9}

Also note that Count can be expensive under heavy concurrency. Prefer consuming until TryTake fails instead of polling count in tight loops.

Choosing between ConcurrentBag, ConcurrentQueue, and ConcurrentStack

Pick by behavior, not by name:

  1. Use ConcurrentQueue for FIFO semantics.
  2. Use ConcurrentStack for LIFO semantics.
  3. Use ConcurrentBag when order is irrelevant and throughput matters most.

If your design requires deterministic processing order, ConcurrentBag is the wrong type even if it compiles and appears fast in small tests.

Parallel aggregation example

A common pattern is collecting exceptions or results from many tasks.

csharp
1using System.Collections.Concurrent;
2
3var errors = new ConcurrentBag<Exception>();
4
5Parallel.ForEach(urls, url =>
6{
7    try
8    {
9        Process(url);
10    }
11    catch (Exception ex)
12    {
13        errors.Add(ex);
14    }
15});
16
17foreach (var ex in errors)
18{
19    Console.WriteLine(ex.Message);
20}

The bag removes lock management burden from application code.

Validation and production readiness

Even with thread-safe containers, correctness still depends on surrounding workflow. Add stress tests that run the same workload repeatedly under high parallelism, then validate invariants after execution. Examples include expected final counts, uniqueness requirements, and allowed error rates. If business logic depends on ordering, tests should fail immediately when non-determinism appears.

Keep diagnostics simple and structured. Log item lifecycle events only at sampled rates in production, because full logging in hot paths can hide the actual concurrency behavior by changing timing. For background services, include health metrics that expose queue or bag growth trends so silent backlog issues are visible before incidents.

Prefer bounded execution models around the bag. An unbounded producer with a slow consumer can still exhaust memory even though the collection itself is thread-safe. Add throttling, cancellation, and graceful shutdown behavior so partial work can be drained safely.

Validation and production readiness

A practical solution should be verified under realistic conditions, not just a single local run. Build a compact test matrix with expected inputs, boundary values, malformed cases, and one representative high-volume scenario. This catches many defects early, including hidden assumptions about ordering, type conversion, timing, and error propagation. If the implementation interacts with external systems, include at least one test where a dependency is unavailable and confirm the failure mode is explicit and observable.

Use deterministic checks wherever possible. For data processing flows, assert row counts, key uniqueness, and output schema. For asynchronous flows, assert completion timing boundaries and cancellation behavior. For security-sensitive operations, assert deny-by-default behavior when configuration is missing or invalid. Do not rely on visual inspection alone. Codified assertions are faster to run and easier to maintain.

text
1validation_matrix:
2  - happy path with representative data
3  - boundary conditions and empty inputs
4  - malformed data and unexpected types
5  - dependency unavailable and timeout path
6  - repeatability check under concurrent runs

Observability is part of correctness. Emit structured logs around key decision points so failures can be diagnosed without reproducing the entire scenario manually. Include identifiers, operation outcome, and duration in a consistent format. Avoid sensitive payloads in logs. For long-running jobs, add periodic progress events and final summary counters so stalled states can be detected quickly.

Configuration should be explicit and versioned. Keep environment-dependent values external and validate them at startup. If a required variable is absent, fail fast with clear messaging instead of silently applying weak defaults. Document compatible runtime versions and dependency constraints near the code to reduce environment drift between local machines and CI runners.

Before release, apply a lightweight operational checklist. Confirm rollback steps, monitor thresholds, and idempotency expectations. If a task can run more than once, ensure repeated execution does not corrupt state or duplicate side effects. Teams that standardize this discipline usually reduce incident frequency and spend less time on reactive debugging.

Common Pitfalls

  • Using ConcurrentBag when FIFO order is required, then debugging random ordering bugs.
  • Treating Count as a stable real-time signal during heavy concurrent mutation.
  • Assuming thread safety of container automatically makes the entire pipeline safe.
  • Forgetting cancellation and backpressure around unbounded producers.
  • Replacing List with ConcurrentBag without reviewing semantic requirements.

Summary

  • ConcurrentBag is best for unordered high-throughput concurrent add and take operations.
  • Use TryTake and TryPeek, and avoid order assumptions.
  • Choose collection type based on required processing semantics.
  • Validate concurrency behavior with stress tests, not just unit tests.
  • Add operational controls such as throttling and metrics for production stability.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.