System designTroubleshooting

Vision software memory leaks: find and fix inspection loop growth

Vision software whose memory grows every inspection cycle leaks a retained buffer: measure bytes per cycle with a 1000-cycle test and reuse loop buffers.

LumaSpec Vision12 min read Share on LinkedIn

On this page 10 sections
  1. 01Why does my vision program's memory grow every cycle?
  2. 02How do I measure the leak before I change any code?
  3. 03What leaks a whole image buffer every cycle?
  4. 04Which smaller objects still add megabytes over a shift?
  5. 05Why does memory grow when a garbage collector is running?
  6. 06How do I fix the loop so memory stays flat?
  7. 07How much memory does a clean inspection loop actually need?
  8. 08When is the leak not in your code at all?
  9. 09How do I keep the leak from coming back?
  10. 10Questions

Memory that climbs a little every inspection cycle is the most common way a working vision program dies: fine at commissioning, sluggish by Wednesday, restarting itself by Friday. The cause is consistent enough that the fix is nearly always the same. The loop allocates an image buffer, an ROI copy, a match-result list or a display object on every cycle, and nothing ever releases it. This guide shows you how to measure bytes leaked per cycle with a 1000-cycle test, name the guilty allocation from the arithmetic alone, rebuild the loop around reused buffers, and prove the result stays flat.

Why does my vision program's memory grow every cycle?

Almost every case is the loop itself: each cycle allocates a new image buffer, an ROI copy, a match-result list or a display object, nothing releases it, and the working set climbs by one retained object per cycle — a few megabytes each is gigabytes per hour at production rates.

Vision programs turn small allocation bugs into fast crashes because the unit of work is enormous. A 5 MP mono frame is 2448 × 2048 = 5,013,504 one-byte pixels, and 5,013,504 ÷ 1,048,576 = 4.78 MB. Retain one frame per cycle at 2 cycles per second and the process grows 4.78 × 2 = 9.6 MB/s, which is 9.6 × 3600 ≈ 34 GB per hour.

Leaks arrive in three families, and every one draws the same straight line in Task Manager: image buffers at megabytes each, display handles and graphics objects at kilobytes each, and result structures from bytes to kilobytes. Only the slope differs, and the slope is what the next section measures.

How do I measure the leak before I change any code?

Run exactly 1000 cycles on the bench, read the process working set in Task Manager before and after, divide the difference by 1000, and treat growth that survives five minutes idle as retained memory rather than collector lag.

  1. Restart the program and run 50 cycles first. Runtimes compile code on first execution and SDKs allocate their buffer pools at start-up, so early growth is warm-up, not a leak.
  2. In Task Manager's Details tab add the Working set (memory), Handles and GDI Objects columns, then note all three.
  3. Run exactly 1000 inspection cycles; a PLC cycle counter or a counter in your own loop both work.
  4. Read the three values again. Bytes leaked per cycle = (after − before) ÷ 1000.
  5. Leave the program idle for five minutes and read once more. Extra memory still resident is retention; memory that fell back was churn — wasteful, but not yet a leak.
leak per cycle (MB) = (working set after − working set before) ÷ cycles run leak rate (MB/s) = leak per cycle × cycles per second time to exhaustion (s) = free memory (MB) ÷ leak rate

A worked example shows how the arithmetic names the allocation. Working set before: 1.2 GB. After 1000 cycles: 6.3 GB. Growth: 6.3 − 1.2 = 5.1 GB = 5222 MB, so 5222 ÷ 1000 = 5.2 MB leaked per cycle. The camera frame accounts for 4.78 MB of it. The remaining 5.2 − 4.78 = 0.42 MB matches a 640 × 480 ROI copy — 640 × 480 = 307,200 bytes = 0.29 MB — plus roughly 0.13 MB of results and handles. One frame and one ROI copy retained per cycle, named before any profiler is opened.

The same numbers predict the crash. At 2 cycles per second the leak rate is 5.2 × 2 = 10.4 MB/s. With 6.5 GB free on the controller, exhaustion arrives after 6656 ÷ 10.4 = 640 s, about 11 minutes of production — which is why these programs die mid-shift rather than at end of shift.

What leaks a whole image buffer every cycle?

The usual culprit is a fresh image object per cycle: a new acquisition buffer, a clone of the camera frame, or a deep copy into a library image; a 5 MP mono frame is 4.78 MB (2448 × 2048 one-byte pixels), and nothing in the loop releases it.

Three code shapes produce it. Acquiring into a new image each grab instead of into a pre-allocated buffer. Cloning the frame so a second algorithm can work while the first runs. Converting or re-encoding on every cycle — mono to color, 8-bit to 16-bit, camera image to library type. Each shape is several megabytes, each runs once per part, and each adds a steady linear slope.

The fix is a shape, not a syntax: create the buffer once before the loop, acquire into it, and hand out views instead of copies. Most SDKs accept a user-supplied grab buffer, and most libraries can cut a zero-copy ROI window onto a frame you already own. For a reference skeleton, the hardware trigger grab loop in Python that acquires and saves each frame keeps one acquisition object and one buffer — extend that shape rather than inventing your own.

Which smaller objects still add megabytes over a shift?

Result objects, ROI copies, overlay graphics and event subscriptions leak in the tens of kilobytes each, but at 2 to 10 cycles per second a 50 kB object is 360 MB to 1.8 GB per hour, so the small stuff ends the shift as surely as the frames do.

  • ROI copies. A 640 × 480 copy is 0.29 MB, and a multi-stage inspection often makes several per cycle.
  • Match results and coordinate lists. Tens of kilobytes, and code that appends history for the log keeps every one.
  • Overlay shapes and bitmaps. Kilobytes each, plus a GDI handle per drawn object.
  • Event subscriptions added per cycle. The delegate is small; the reference it keeps to the previous full frame is not, so each new subscription roots an entire image.
  • Per-cycle feature handles. Opening a fresh camera-feature or node object each iteration to set exposure or gain leaks the handle as surely as an image leaks a buffer. Setting and getting camera parameters in a capture loop safely shows the cached-handle pattern, and camera feature access memory leak troubleshooting walks this failure mode end to end.

Why does memory grow when a garbage collector is running?

Because most of a vision program's memory is native: the SDK's acquisition buffers, library images and window handles live outside the managed heap, the collector cannot see or free them, disposing the wrapper object is what releases the megabytes, and skipping that call leaks them every cycle.

Three mechanisms do the damage. Native ownership: every image handed to a vision library is native memory behind a thin managed wrapper, finalizers run late or never on abandoned objects, so a Dispose or Release call in a finally block is the only deterministic free. The large object heap: allocations over 85 kB go there, which is every full frame, so a fresh frame per cycle fragments the heap even when collection eventually succeeds. Handles: Windows caps GDI objects at 10,000 per process by default, and a window or bitmap created per cycle walks steadily toward that wall — the UI misdraws before the process dies, and the GDI Objects column from the measurement test is the tell.

Both ownership models have their own pages here: managing image buffer ownership to stop C# camera memory leaks and preventing memory leaks when calling a vision library from C#.

How do I fix the loop so memory stays flat?

Allocate every buffer once before the loop and reuse it every cycle: acquire into the same image, take ROI views instead of copies, clear and refill result containers, update one persistent window instead of opening new ones, and unsubscribe anything the loop subscribes.

  1. Inventory the loop body: every new image, clone, ROI copy, result list, window, overlay and subscribe call.
  2. Create image buffers once, before the loop, and acquire into them on every cycle.
  3. Replace ROI copies with views where the library allows; where a copy is unavoidable, write into one reused ROI buffer.
  4. Clear and refill result containers instead of constructing new ones.
  5. Create display windows and overlays once at start-up and update their content per cycle.
  6. Give every native wrapper a Dispose or Release in a finally block, or scope it in a using.
  7. Subscribe once at start-up; unsubscribe only in shutdown code.

The build-inside-the-loop habit is not unique to vision; the same measure-and-fix sequence applies in plant control software, as a knowledgebase walkthrough of while-loop object memory leaks shows.

Re-run the 1000-cycle test and hold the pass mark at under 1 MB of growth across 1000 cycles — about 1 kB per cycle, which is counter noise and log flushes rather than retention. In the worked example the repaired loop sat flat at 1.3 GB from cycle 50 through cycle 1000, growth under 0.1 MB. Check the Handles and GDI Objects columns too: a memory fix that lets either count climb has moved the leak, not fixed it.

How much memory does a clean inspection loop actually need?

Size it from the buffers you keep: frames in flight × frame size, plus templates, plus a program base of 300-500 MB — a two-camera 5 MP line keeping two frames per camera holds 4 × 4.78 MB = 19 MB of images, so a healthy working set sits near 0.5 GB.

steady-state working set ≈ program base + frames kept × frame size + templates

Run the budget end to end. Four frames in flight: 4 × 4.78 MB = 19 MB. Ten 256 × 256 grayscale pattern templates: 10 × 65,536 bytes = 0.6 MB. Program base — runtime, UI, algorithm libraries, a bounded log cache — lands between 300 and 500 MB on a typical controller. Total: roughly 0.5 GB steady-state. If Task Manager shows 3 GB on the same two-camera line, about 2.5 GB is retained; go back to the measurement test for a fresh reading.

Resolution scales the stakes linearly: a 12 MP frame is about 12 MB at 8 bits per pixel, a 25 MP frame about 25 MB, and every pipeline stage that keeps its own copy multiplies the size again. The budget, not hope, sizes the controller. For a one- or two-camera 5 MP line the entry tier carries it comfortably. The MV-VC3301H-128G66 carries 8 GB of memory and includes a VM6200Pro license.

Vision controllersHikrobot MV-VC3301H-128G66 I3-8100, 8GB+128G SSD, No PoE, PCIe expansion supported, Incl. VM6200Pro licenseMV-VC3301H-128G66
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
Light outputs
4 built-in channels
Memory
8 GB
$2,419.00View partBuild a system with it

That 8 GB leaves more than 7 GB for the operating system, the HMI and the archive cache on a 0.5 GB loop. Multi-camera lines and 12-25 MP sensors belong a tier up. The MV-VC4712-256G10-2TH pairs an Ultra 7 255H processor with 2 × 16 GB of memory and PCIe expansion.

Vision controllersHikrobot MV-VC4712-256G10-2TH Ultra 7 255H, 16G×2+256GSSD+2THDD, PCIe expansion, Without VM license keyMV-VC4712-256G10-2TH
LAN ports
3-4 LAN ports
USB 3 ports
2 USB 3 ports
PoE
PoE camera ports
Light outputs
6 built-in channels
$6,049.00 · quotedView and quoteBuild a system with it

Four or more cameras with PoE cabling and the largest model libraries take the top tier. The MV-VC5525-256G70-NN carries 32 GB of memory and PoE camera ports.

Vision controllersHikrobot MV-VC5525-256G70-NN VC5000 Vision Controller, 12th-I5, 32GB+256G SSD, Supported PCIEMV-VC5525-256G70-NN
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
PoE
PoE camera ports
Light outputs
8 built-in channels
$4,479.00 · quotedView and quoteBuild a system with it

When is the leak not in your code at all?

Two cases stand out: a driver, SDK or camera firmware release with a known and already fixed leak, and a second process holding the camera or its port. Read the vendor's release notes for fixed memory-leak entries and update before profiling, and confirm exactly one application owns each camera.

Release notes earn their place: vendor readme files across the industry list "[Fixed] memory leak when using..." entries routinely, and an afternoon of updating firmware and SDK versions costs less than a week profiling someone else's bug. Re-run the 1000-cycle test after updating; sometimes the slope is simply gone.

Two neighbours masquerade as the leak. A second application holding the camera produces corruption and doubled buffers rather than clean growth — the one-camera-per-process rule is short enough to memorize, and running multiple vision software instances with one camera per process covers the failure modes. Growth that only advances while images are being saved is the archive, not the heap; camera logging memory growth: diagnose and bound log storage shows how to bound it.

If the inspection is simple enough to run entirely on-device, smart cameras move the loop into vendor firmware, where your only leak fix is an update; the smart camera finder filters by resolution, shutter and interface. Custom logic that outgrows a smart camera belongs on a vision controller, where the 1000-cycle test applies to code you own.

How do I keep the leak from coming back?

Add the 1000-cycle soak test to every software release and alert on the trend: growth over 1 MB across 1000 cycles or 100 MB in a 10-hour shift stops the release, because a defect caught at the bench costs minutes while a mid-shift restart costs the order.

Make the checks routine rather than heroic:

  • Soak 1000 cycles after every change to loop code, SDK version, driver or camera firmware.
  • Poll the working set once per minute in production and alarm well before the crash; dashboard camera monitoring for every application shows the trend without a spreadsheet.
  • Review every change that touches acquisition for allocations introduced inside the loop.
  • Bound every collection that grows with runtime: image archive, result history, log buffer.
  • Re-run the budget after any camera or resolution change; a 12 MP frame moves about 2.4 times the bytes of the 5 MP frame in these examples, so the same leak costs 2.4 times as much per cycle.

If a growth graph on your line resists this sequence, bring the before and after Task Manager readings and a loop outline through the contact page and we will work the allocation inventory with you.

Questions

Does memory growth always mean there is a leak?

Not in the first minute. Runtimes compile code, SDKs allocate their acquisition buffer pools, and caches fill on start-up, so a fresh process grows legitimately at first. Plot the readings: warm-up growth decelerates to a plateau within the first few hundred cycles, while a leak holds a constant slope for thousands. Only the straight line needs a fix.

How much leaked memory is acceptable in production?

Hold the line at near-flat: under 1 MB of growth across a 1000-cycle soak, which is under 1 kB per cycle. Even that slope adds up on continuous running: 1 kB per cycle at 2 cycles per second is 2 kB/s, and a year holds about 31,536,000 s, so the same leak is roughly 63 GB per year. Treat any measurable slope as a defect to schedule, not a margin to spend.

Does a 64-bit operating system make leaks harmless?

No. A 64-bit process can address far more virtual space than it has physical RAM, but installed memory still bounds it. Once the working set passes installed RAM, Windows pages to disk and cycle time climbs first, so erratic inspection times usually appear before the out-of-memory crash. Rising cycle time with a flat image is an early memory alarm.

What working-set level should raise an alarm or stop the line?

On a dedicated vision controller, raise a maintenance alarm at 70 percent of installed physical RAM and stop the line in a controlled way at 85 percent. The gap is roughly one shift of notice at typical leak rates: enough to finish the order, archive the logs and restart inside a planned window instead of losing the machine mid-cycle.

Is a scheduled nightly restart an acceptable fix?

It is a bridge, not a fix. A 2 a.m. restart converts an unpredictable crash into planned downtime and bounds the damage, which can be the right move this week. The leak stays in the code, the restart window shrinks as the leak grows, and the 1000-cycle soak test still has to happen before the schedule comes off.