How to Use Async and Multithreading for QR Code Operations in C#

Single-threaded QR scanning blocks the calling thread for the duration of every image decode. In a WPF button handler that freezes the UI, or a batch job that processes 500 images sequentially when 8 CPU cores sit idle, the fix is the same: move decoding off the calling thread. IronQR provides ReadAsync for non-blocking single reads and a standard Read method that pairs naturally with Parallel.ForEach and Task.WhenAll for batch throughput.

We cover async reading, parallel batch processing, and a combined pattern with controlled concurrency below.

Quickstart: Process QR Codes Asynchronously

Load an image, pass it to ReadAsync, and await the decoded results without blocking the calling thread.

  1. Install IronQR with NuGet Package Manager

    PM > Install-Package IronQR
  2. Copy and run this code snippet.

    using IronQr;
    using IronSoftware.Drawing;
    
    var input = new QrImageInput(AnyBitmap.FromFile("ticket.png"));
    IEnumerable<QrResult> results = await new QrReader().ReadAsync(input);
    Console.WriteLine(results.First().Value);
  3. Deploy to test on your live environment

    Start using IronQR in your project today with a free trial

    arrow pointer

How to Read QR Codes Asynchronously?

QrReader.ReadAsync(IQrInput) returns Task<IEnumerable<QrResult>>, making it directly awaitable in async contexts — WPF/MAUI event handlers, ASP.NET controller actions, or any async Task method. The input must be constructed as a QrImageInput from an AnyBitmap; there is no file-path overload.

The write side is different: QrWriter.Write() is synchronous, and QrCode.Save() returns AnyBitmap synchronously — IronQR does not expose a SaveAsync method. To avoid blocking on the file I/O portion of a generate-and-save workflow, we wrap the save step with File.WriteAllBytesAsync().

:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/async-read-write.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;

// --- Async read: non-blocking QR decode ---
var inputBmp = AnyBitmap.FromFile("event-badge.png");
var imageInput = new QrImageInput(inputBmp, QrScanMode.OnlyDetectionModel);

var reader = new QrReader();
IEnumerable<QrResult> results = await reader.ReadAsync(imageInput);

foreach (QrResult result in results)
{
    Console.WriteLine($"[{result.QrType}] {result.Value}");
}

// --- Async-wrapped save: QrWriter.Write() and QrCode.Save() are synchronous ---
QrCode qrCode = QrWriter.Write("https://ironsoftware.com");
AnyBitmap qrImage = qrCode.Save();

// Save the bitmap bytes asynchronously (not an IronQR API — standard .NET async I/O)
byte[] pngBytes = qrImage.ExportBytes();
await File.WriteAllBytesAsync("output-qr.png", pngBytes);
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.IO

' --- Async read: non-blocking QR decode ---
Dim inputBmp = AnyBitmap.FromFile("event-badge.png")
Dim imageInput = New QrImageInput(inputBmp, QrScanMode.OnlyDetectionModel)

Dim reader = New QrReader()
Dim results As IEnumerable(Of QrResult) = Await reader.ReadAsync(imageInput)

For Each result As QrResult In results
    Console.WriteLine($"[{result.QrType}] {result.Value}")
Next

' --- Async-wrapped save: QrWriter.Write() and QrCode.Save() are synchronous ---
Dim qrCode As QrCode = QrWriter.Write("https://ironsoftware.com")
Dim qrImage As AnyBitmap = qrCode.Save()

' Save the bitmap bytes asynchronously (not an IronQR API — standard .NET async I/O)
Dim pngBytes As Byte() = qrImage.ExportBytes()
Await File.WriteAllBytesAsync("output-qr.png", pngBytes)
$vbLabelText   $csharpLabel

The QrScanMode.OnlyDetectionModel parameter tells the reader to use the ML detection model exclusively, which is faster per image than QrScanMode.Auto (ML + basic scan). For single-image reads in a UI context, the speed difference is small; for batches, it compounds meaningfully.

Note the explicit comment on the save step: File.WriteAllBytesAsync() is standard .NET, not an IronQR method. We call this out because mixing IronQR's synchronous Save() with an await on the I/O portion is a common source of confusion. The ExportBytes() method on AnyBitmap provides the raw PNG byte array for the async write.


How to Process QR Codes with Multithreading?

Batch scanning benefits from Parallel.ForEach when each image can be processed independently. We create a new QrReader instance per iteration as the thread-safe default — IronQR's documentation does not make an explicit thread-safety guarantee for shared reader instances, so per-thread construction is the conservative choice.

:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/parallel-batch.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using System.Collections.Concurrent;
using System.Diagnostics;

string[] files = Directory.GetFiles("qr-images/", "*.png");
var allResults = new ConcurrentBag<(string File, string Value)>();
int failCount = 0;
var sw = Stopwatch.StartNew();

Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, file =>
{
    try
    {
        var input = new QrImageInput(
            AnyBitmap.FromFile(file),
            QrScanMode.OnlyDetectionModel);

        // Per-thread QrReader instance — safe default
        var results = new QrReader().Read(input);

        foreach (QrResult result in results)
        {
            allResults.Add((Path.GetFileName(file), result.Value));
        }
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failCount);
        Console.Error.WriteLine($"[ERROR] {Path.GetFileName(file)}: {ex.Message}");
    }
});

sw.Stop();

Console.WriteLine($"Processed {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s");
Console.WriteLine($"QR codes found: {allResults.Count} | Failures: {failCount}");
Console.WriteLine($"Throughput: {files.Length / sw.Elapsed.TotalSeconds:F1} files/sec");
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.Collections.Concurrent
Imports System.Diagnostics

Dim files As String() = Directory.GetFiles("qr-images/", "*.png")
Dim allResults As New ConcurrentBag(Of (File As String, Value As String))()
Dim failCount As Integer = 0
Dim sw As Stopwatch = Stopwatch.StartNew()

Parallel.ForEach(files, New ParallelOptions With {.MaxDegreeOfParallelism = Environment.ProcessorCount}, Sub(file)
    Try
        Dim input As New QrImageInput(AnyBitmap.FromFile(file), QrScanMode.OnlyDetectionModel)

        ' Per-thread QrReader instance — safe default
        Dim results = New QrReader().Read(input)

        For Each result As QrResult In results
            allResults.Add((Path.GetFileName(file), result.Value))
        Next
    Catch ex As Exception
        Interlocked.Increment(failCount)
        Console.Error.WriteLine($"[ERROR] {Path.GetFileName(file)}: {ex.Message}")
    End Try
End Sub)

sw.Stop()

Console.WriteLine($"Processed {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s")
Console.WriteLine($"QR codes found: {allResults.Count} | Failures: {failCount}")
Console.WriteLine($"Throughput: {files.Length / sw.Elapsed.TotalSeconds:F1} files/sec")
$vbLabelText   $csharpLabel

The ConcurrentBag<T> collects results across threads without locking. Interlocked.Increment safely counts failures. The per-file try-catch ensures one corrupt image does not abort the entire batch — the same error-isolation pattern described in the error handling how-to.

Setting MaxDegreeOfParallelism to Environment.ProcessorCount caps thread creation to available cores. Oversubscribing beyond this adds context-switch overhead without improving throughput, particularly for the ML model which is CPU-bound.

The QrScanMode passed to QrImageInput controls per-image cost and compounds across the batch. OnlyDetectionModel runs the ML model alone — fastest for clean printed codes. Auto runs both ML detection and a basic scan pass, trading speed for accuracy on damaged or skewed codes. OnlyBasicScan bypasses ML entirely and uses only the traditional decoder. For batch processing of machine-printed labels where image quality is consistent, OnlyDetectionModel provides the best throughput. For document scanning where QR codes may be partially obscured, Auto is the safer choice at the cost of roughly 2× per-image time.


How to Combine Async and Parallel Processing?

The production pattern for high-volume pipelines uses SemaphoreSlim to bound concurrency while running ReadAsync inside a Task.WhenAll fan-out. This gives us non-blocking I/O (image loading) combined with controlled CPU parallelism (ML decoding), without over-saturating the thread pool.

:path=/static-assets/qr/content-code-examples/how-to/async-and-multithreading/semaphore-pipeline.cs
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using System.Collections.Concurrent;
using System.Diagnostics;

string[] files = Directory.GetFiles("high-volume/", "*.png");
var results = new ConcurrentBag<(string File, string Value)>();
int maxConcurrency = Environment.ProcessorCount;
using var semaphore = new SemaphoreSlim(maxConcurrency);
var sw = Stopwatch.StartNew();

var tasks = files.Select(async file =>
{
    await semaphore.WaitAsync();
    try
    {
        var bmp = AnyBitmap.FromFile(file);
        // OnlyDetectionModel: fastest per-image — critical at scale
        var input = new QrImageInput(bmp, QrScanMode.OnlyDetectionModel);
        var qrResults = await new QrReader().ReadAsync(input);

        foreach (var qr in qrResults)
        {
            results.Add((Path.GetFileName(file), qr.Value));
        }
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine($"{{\"file\":\"{Path.GetFileName(file)}\",\"error\":\"{ex.Message}\"}}");
    }
    finally
    {
        semaphore.Release();
    }
});

await Task.WhenAll(tasks);
sw.Stop();

Console.WriteLine($"Pipeline complete: {results.Count} QR codes from {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s");
Imports IronQr
Imports IronQr.Enum
Imports IronSoftware.Drawing
Imports System.Collections.Concurrent
Imports System.Diagnostics

Module Program
    Sub Main()
        Dim files As String() = Directory.GetFiles("high-volume/", "*.png")
        Dim results As New ConcurrentBag(Of (File As String, Value As String))()
        Dim maxConcurrency As Integer = Environment.ProcessorCount
        Using semaphore As New SemaphoreSlim(maxConcurrency)
            Dim sw As Stopwatch = Stopwatch.StartNew()

            Dim tasks = files.Select(Function(file) Task.Run(Async Function()
                                                                  Await semaphore.WaitAsync()
                                                                  Try
                                                                      Dim bmp = AnyBitmap.FromFile(file)
                                                                      ' OnlyDetectionModel: fastest per-image — critical at scale
                                                                      Dim input As New QrImageInput(bmp, QrScanMode.OnlyDetectionModel)
                                                                      Dim qrResults = Await (New QrReader()).ReadAsync(input)

                                                                      For Each qr In qrResults
                                                                          results.Add((Path.GetFileName(file), qr.Value))
                                                                      Next
                                                                  Catch ex As Exception
                                                                      Console.Error.WriteLine($"{{""file"":""{Path.GetFileName(file)}"",""error"":""{ex.Message}""}}")
                                                                  Finally
                                                                      semaphore.Release()
                                                                  End Try
                                                              End Function))

            Task.WhenAll(tasks).Wait()
            sw.Stop()

            Console.WriteLine($"Pipeline complete: {results.Count} QR codes from {files.Length} files in {sw.Elapsed.TotalSeconds:F1}s")
        End Using
    End Sub
End Module
$vbLabelText   $csharpLabel

The QrScanMode choice matters at scale. OnlyDetectionModel runs the ML detection model alone, which is optimized for speed — ideal for camera frames and batch processing. Auto combines ML with a basic scan pass for higher accuracy on damaged codes, at roughly 2× the per-image cost. OnlyBasicScan skips ML entirely and uses only the traditional decoder. For a production pipeline processing clean printed QR labels, OnlyDetectionModel provides the best throughput-to-accuracy tradeoff.


What Are My Next Steps?

We covered ReadAsync for non-blocking QR reads, Parallel.ForEach for CPU-bound batch throughput, and SemaphoreSlim + Task.WhenAll for bounded async parallelism. The QrScanMode enum controls the speed–accuracy tradeoff at the per-image level, and OnlyDetectionModel is the right default for high-volume pipelines.

For further reading:

View licensing options when the pipeline is ready for production.

Curtis Chau
Technical Writer

Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.

...

Read More
Ready to Get Started?
Nuget Downloads 60,166 | Version: 2026.3 just released
Still Scrolling Icon

Still Scrolling?

Want proof fast? PM > Install-Package IronQR
run a sample watch your URL become a QR code.