zplCore.comzplCore.com

Platforms & cloud

Runs wherever your backend runs

One .NET 10 SDK, no printer driver, shipped with dotnet publish like any other dependency: on Windows, Linux and macOS, in IIS, in Docker containers and Kubernetes clusters, on-premises or in clouds such as Azure and AWS.

  • Windows
  • Linux
  • macOS
  • Docker
  • Kubernetes
  • Raspberry Pi
  • Synology NAS
  • IIS
  • Azure
  • AWS
VerifiedWindowsLinuxmacOS

Windows · Linux · macOS

A single .NET 10 assembly built on SkiaSharp and HarfBuzz. The automated test suite passes on Windows 11, macOS and Linux. No printer driver and no GUI required.

Cloud & edgeDockerKubernetesLinuxRaspberry Pi

Containers, NAS & Pi

Tested in the .NET images for Debian, Ubuntu chiseled and Azure Linux 3.0 (distroless & NativeAOT). Even an existing NAS such as a Synology DS923+ (Container Manager) or a Raspberry Pi turns into a local label print server with zero extra system packages.

Sample

Minimal API sample

ASP.NET Core sample with streaming endpoints, a bounded queue (Channel + BackgroundService) and parallel workers.

Docker & Kubernetes: Dockerfile and Helm chart included

  • All tests pass in the official .NET 10 container images for Debian, Ubuntu 24.04 chiseled and Azure Linux 3.0.
  • Dockerfile.server included: distroless image, runs without root privileges, with a /health endpoint (or as NativeAOT on runtime-deps:10.0-noble-chiseled-extra from ~79 MB).
  • Helm chart for Kubernetes included. You build the image yourself; it is not published to any public registry.
  • No extra system packages required; the NuGet package ships the native libraries.
  • Turns an existing NAS (e.g. a Synology DS923+ via Container Manager, ~90 MiB RAM, 295 PNG labels/s with NativeAOT) or a Raspberry Pi right into a local warehouse label print server.
  • Asynchronous ASP.NET Core minimal API sample with a job queue (199,000 labels/s ZPL generation on an Apple M3 Max, small 100 × 60 mm label; real shipping label: 200,000+ labels/s).
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish samples/WebApiDemo -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0-distroless
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet","WebApiDemo.dll"]

Performance

Fast enough for the 6 p.m. shipping cutoff

Every number is measured, not estimated: release build, .NET 10, Apple M3 Max (10 performance + 4 efficiency cores). The benchmarks ship with the repository.

Real shipping label: 200,000+ labels per second

Measured on 21 Oct 2026 with samples/ShippingBench: Apple M3 Max (10 performance + 4 efficiency cores, 14 in total), macOS 26.6, .NET 10, release build, workstation GC. End to end from the records to a ZPL file on disk, median of 5 runs after one warm-up run. All data sources write a byte-identical 131 MB file (SHA-256 checked), under JIT and NativeAOT alike.

200,000+shipping labels/sdictionary or JSON records → ZPL file, JIT and NativeAOT
< 0.5 s100,000 shipping labelsdictionary 0.40 s, JSON 0.46 s (median)
1,000+PNG previews/sall 14 cores, 203 dpi (1,224 JIT / 1,182 NativeAOT)
100,000+labels/s from C# objectsPOCO binding, JIT (124,995/s)
The benchmark label: 100 × 150 mm shipping label with sender, EXPRESS service box, recipient, routing code, weight, parcels, ship date, QR code, Code 128 tracking number and GS1-128 SSCC
The test label at 203 dpi, one of the 100,000 records. Click for 300 dpi.

The label

A 100 × 150 mm (4 × 6 in) shipping label with 33 elements: sender block, inverted service box, recipient (name, street, city, country via expression), routing code, weight (0.0 kg), parcels, ship date (Format(ToDate([ShipDate]), "dd.MM.yyyy")), QR code (tracking URL as expression), Code 128 (tracking number), GS1-128 SSCC (18 digits with check digit), page counter, frames and rules. The print job adds a start label (tour, depot, total weight and parcels, first/last SSCC) and an end label (label count, first/last key).

The records vary like real data: names with umlauts and double names, company suffixes of different length, flat numbers, 9 places in AT/DE/CH/IT, 6 services, 2.5–29 kg, 1–7 parcels, valid SSCCs.

Method

  • Records → ZPL → file on disk (StreamWriter, 64 KB buffer), like a print server or batch job.
  • Byte-identical output across all data sources, checked by SHA-256 before measuring; otherwise the run aborts.
  • Same data, same order, same machine; one warm-up run, then the median of 5 runs.

Downloads

  • Label: PDF (vector, 1 page, 116 KB) · ZPL (203 dpi, 1.3 KB)
  • Print job (start label + 3 shipments + end label): PDF (5 pages, 615 KB) · ZPL (300 dpi, 6.3 KB)
  • Image: PNG 203 dpi · PNG 300 dpi

100,000 shipping labels + start and end label into one ZPL file. JIT and NativeAOT build of the same code, median of 5 runs.

Data sourceJIT labels/sNativeAOT labels/s100,000 labels take
IReadOnlyDictionary<string, object?> records249,286238,5900.40–0.42 s
JSON records (JsonElement)216,171213,6370.46–0.47 s
POCO with Map() per field128,16591,7840.78–1.09 s
POCO, For<T>() (reflection)124,99583,9940.80–1.19 s
Stored format (^DF/^XF), POCO109,53172,1130.91–1.39 s

For mass data, pass dictionaries or JSON: they stay above 200,000 labels/s under JIT and NativeAOT (slowest single run 208,068/s). C# objects (POCO) reach about 125,000/s under JIT and about 84,000/s under NativeAOT.

Measure it yourself

The benchmark ships with the repository. sample writes the label as PNG, PDF and ZPL; the Docker image is built from build/Dockerfile.shippingbench. Figures depend on your hardware; they are measurements, not a guarantee.

# JIT
dotnet run -c Release --project samples/ShippingBench
# NativeAOT (same code)
dotnet publish samples/ShippingBench -c Release -r osx-arm64 -p:PublishAot=true -o out
# Docker (NativeAOT) + 5 million JSON Lines records
docker build -f build/Dockerfile.shippingbench -t zplcore-shippingbench .
docker run --rm -v zplcore-bench:/data zplcore-shippingbench 1000 10 1 5000000
# label as PNG/PDF/ZPL
dotnet run -c Release --project samples/ShippingBench -- sample out/sample

More benchmarks: small 7-element label

The following figures come from samples/PerfTest, samples/RenderBench and samples/WebApiDemo, mostly with a small 100 × 60 mm label (PerfTest: 7 elements, i.e. text, expression, number format, Code 128, counter), not with the shipping label above. Same Apple M3 Max.

199,000labels/s ZPLstored format, 100,000 labels in one job (LiteDB → file: 0.50 s)
5,022PNG renderings/s10 threads, render + PNG, 9.2× vs. 1 thread
24.8 s100,000 PNG previewsparallel on 14 cores, 4,033 labels/s (sequential 3.4 min)
1Mlabels without a leakmemory stays flat, GC heap 2–3 MB

Small 7-element label (100 × 60 mm), not the shipping label above. One print job: leading label + 100,000 labels + trailing label, cut only after the last label. 14 cores, samples/PerfTest, single run.

ScenarioTimeLabels/sSizeMemory (heap)
Objects → ZPL, stored format (^DF/^XF)0.73 s136,00014.7 MB24 MB
Objects → ZPL, full format per label0.39 s256,00029.1 MB24 MB
EF Core / SQLite AsAsyncEnumerable() → file0.57 s176,00014.7 MB29 MB
LiteDB query → file0.50 s202,00014.7 MB64 MB
PNG preview, sequential (799 × 480 px, grayscale)3.4 min49018.2 MB / 2,00084 MB
PNG preview, parallel (14 cores)24.8 s4,03317.1 MB / 2,000106 MB

ZPL generation is practically free and streams with constant memory. The stored format halves the data sent to the printer (14.7 instead of 29.1 MB).

Methodology & edge/NAS containers

Release build, .NET 10, Apple M3 Max (10 performance + 4 efficiency cores). Also measured inside a NativeAOT container (runtime-deps:10.0-noble-chiseled-extra, ~79 MB) on a Synology DS923+ NAS (AMD Ryzen R1600, 2 cores / 4 threads): 237,428 labels/s parsing, 1,233 labels/s bitmap rendering and 295 PNG labels/s (> 17,500 PNGs/min). Every measurement can be reproduced with a single command.

# 100,000-label print job
dotnet run -c Release --project samples/PerfTest -- 100000
# Thread scaling (label|text|barcode|box|empty)
dotnet run -c Release --project samples/RenderBench -- 150 scale