# Mail Merge Performance

> Measured MailMerge times, faster merge block design, background merges in Windows Forms with DataRowMerged progress and Cancel, and processes for large batches.

- **Product:** TX Text Control .NET for Windows Forms
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET for Windows Forms > Performance > Mail Merge Performance
- **2727 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/mail-merge/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/mail-merge/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/mail-merge/llms-full.txt

---

How long does a merge take, and what makes it longer? Text Control has measured `MailMerge` with typical invoice templates, and the numbers show which parts of a template cost time and how far one thread goes. The same measurements lead to three practices for Windows Forms applications: design templates with the costs in mind, merge in the background with visible progress, and move large batches into separate processes.

### What Text Control Measured

Text Control's `MailMerge` benchmark of 2023 merged an invoice template with a header, an SVG logo, an address and a merge block of line items, and saved the result as PDF. It ran as a .NET console application with `ServerTextControl` and `MailMerge` of TX Text Control .NET Server 31.0 SP2, on one thread, on an AMD Ryzen 7 2700X eight-core processor with 3.70 GHz and 16 GB of memory:

| Scenario | Result |
|---|---|
| One invoice of one page with 5 rows in the merge block | 0.35 seconds; the time grows linearly with the number of invoices, 80 invoices took about 16 seconds |
| One invoice of six pages with 100 rows in the merge block | 3.2 seconds, again growing linearly with the number of invoices |
| One merge block with a growing number of rows | 0.23 seconds for one row, growing faster than the number of rows up to 1,280 rows on 59 pages |
| The same rows split into two merge blocks | Faster than the same number of rows in one block |
| Two formulas in the merge block | Acceptable for short tables; the formulas are calculated again for every row |
| Encryption, digital signatures and embedded files in the PDF | No significant extra time; embedded files depend on the size of the file |

These numbers were measured with the server product and its hardware; they show proportions, not what your application will reach. The blog article [MailMerge Class Performance Benchmark](https://www.textcontrol.com/blog/2023/04/21/mailmerge-performance-benchmark/) contains the charts and the code.

### Design Templates With the Costs in Mind

- **Keep long tables in several merge blocks.** A single merge block took disproportionately longer as its rows grew, and the same rows in two blocks merged faster. When a report lists thousands of items, group them, for example by month or category, into separate blocks. [Merge Blocks](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/mail-merge/blocks/llms-full.txt) describes how blocks are built.
- **Calculate long columns in the data.** Formulas in a merge block are calculated for every row. For a table with many rows, pass totals and line amounts in the data instead of calculating them in the template.
- **Create separate documents when you need them.** With `append` set to `false`, each record becomes a document of its own, which the `DataRowMerged` event delivers, as [Create One Document or One per Record](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/mail-merge/overview/#create-one-document-or-one-per-record) shows.

### Merge in the Background With Progress and Cancel

A `MailMerge` whose `TextComponent` is the `TextControl` of the form merges on the UI thread, and the form doesn't respond until the merge ends. Connected to a `ServerTextControl`, it can merge on a background thread. Its [DataRowMerged](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/reference/txtextcontrol-documentserver/mailmerge/datarowmerged-event/llms-full.txt) event occurs after each record, which makes it the place to report progress and to cancel.

The following handlers in *Form1.cs* merge all records of *invoice.json* into the template *invoice.docx*, move a `ProgressBar` named `progressBar1`, stop when the user clicks `cancelMergeButton` and show the result in `textControl1`:

```csharp
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using TXTextControl;
using TXTextControl.DocumentServer;

private CancellationTokenSource mergeCancellation;

private async void mergeButton_Click(object sender, EventArgs e)
{
    string json = File.ReadAllText("invoice.json");

    using (JsonDocument data = JsonDocument.Parse(json))
    {
        progressBar1.Maximum = data.RootElement.GetArrayLength();
    }

    progressBar1.Value = 0;
    mergeButton.Enabled = false;

    IProgress<int> progress = new Progress<int>(count => progressBar1.Value = count);
    mergeCancellation = new CancellationTokenSource();
    CancellationToken token = mergeCancellation.Token;

    try
    {
        byte[] result = await Task.Run(() =>
        {
            using (ServerTextControl tx = new ServerTextControl())
            {
                tx.Create();

                LoadSettings loadSettings = new LoadSettings
                {
                    ApplicationFieldFormat = ApplicationFieldFormat.MSWord,
                    LoadSubTextParts = true
                };

                tx.Load("invoice.docx", StreamType.WordprocessingML, loadSettings);

                using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
                {
                    int merged = 0;

                    // Runs on this background thread after each record
                    mailMerge.DataRowMerged += (s, args) =>
                    {
                        progress.Report(++merged);
                        args.Cancel = token.IsCancellationRequested;
                    };

                    mailMerge.MergeJsonData(json);
                }

                tx.Save(out byte[] document, BinaryStreamType.InternalUnicodeFormat);
                return document;
            }
        });

        textControl1.Load(result, BinaryStreamType.InternalUnicodeFormat);
    }
    catch (MergeCanceledException)
    {
        MessageBox.Show(this, "The merge was canceled.");
    }
    finally
    {
        mergeButton.Enabled = true;
        mergeCancellation.Dispose();
        mergeCancellation = null;
    }
}

private void cancelMergeButton_Click(object sender, EventArgs e)
{
    mergeCancellation?.Cancel();
}
```

The code works as follows:

- The template is loaded with `LoadSettings` that import the MS Word fields and the merge blocks of the DOCX file, as in [Merge Data into Fields](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/getting-started/mail-merge/fields/llms-full.txt). [MergeJsonData](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/reference/txtextcontrol-documentserver/mailmerge/mergejsondata-method/llms-full.txt) merges all records into one document.
- The `DataRowMerged` handler runs on the thread that merges, not on the UI thread. It doesn't touch the form: `Progress<int>` passes the count to the UI thread.
- Setting [Cancel](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/reference/txtextcontrol-documentserver/mailmerge-datarowmergedeventargs/cancel-property/llms-full.txt) of the event arguments to `true` ends the merge with a [MergeCanceledException](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/reference/txtextcontrol-documentserver/mergecanceledexception/llms-full.txt), which `await` throws again on the UI thread.
- The merged document returns as a byte array and is loaded into the editor on the UI thread.

For long merges, [MailMerge.MergeCulture](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/reference/txtextcontrol-documentserver/mailmerge/mergeculture-property/llms-full.txt) matters too: it defaults to the culture of the current thread, which on a background thread may not be the one of your form. [UI Threading and Background Operations](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/threading/#how-far-threads-take-you) explains why.

### Merge Large Batches in Separate Processes

Threads that merge in the same process can wait for each other: TX Text Control guards resources that all its instances in a process share with critical sections. For merges of hundreds or thousands of documents, Text Control therefore recommends several processes instead of several threads. In its sample of 2019, a host application passed each template and its data to a new process of a processing application, which merged it with `ServerTextControl` and `MailMerge` and returned the PDF through anonymous pipes. Merging 100 templates took about 45 seconds with normal processing and 14 seconds with parallel processes on a CPU with 16 cores. The sample is a Windows Forms application; the blog article [True Parallel Processing of Documents using MailMerge](https://www.textcontrol.com/blog/2019/08/16/true-parallel-processing-of-documents-using-mailmerge/) describes it and its code.

Text Control also publishes the NuGet package `TXTextControl.DocumentServer.ParallelMailMerge`, which runs `MailMerge` jobs in a pool of worker processes that your application starts. It names Windows Forms applications among the project types it can be used in, but it targets .NET 10 and depends on `TXTextControl.TextControl.Core.SDK`, the package of TX Text Control .NET Server, not on `TXTextControl.TextControl.WinForms.SDK`. [Multiple Processes](https://docs.textcontrol.com/tx-text-control-dotnet-server/performance/multiple-processes/llms-full.txt) in the documentation of TX Text Control .NET Server describes the package, its options and its measurements.

### Next Steps

- [Overview](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/mail-merge/overview/llms-full.txt) — where a merge runs and how its results are created.
- [Merge Events](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/mail-merge/events/llms-full.txt) — all events of `MailMerge`.
- [UI Threading and Background Operations](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/threading/llms-full.txt) — threads, progress and cancellation in Windows Forms.
- [Performance Best Practices](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/best-practices/llms-full.txt) — all measures at a glance.

---

## Nearby

- **Previous:** [Document Loading and Conversion](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/loading-conversion/llms-full.txt)
- **Next:** [Performance Best Practices](https://docs.textcontrol.com/tx-text-control-dotnet-winforms/performance/best-practices/llms-full.txt)
