A C# inspection program that runs for hours, gains memory with every cycle, and finally crashes with an out-of-memory error needs an ownership audit. Managed wrappers around native vision libraries are one possible source, but retained managed objects and growing frame queues can produce similar symptoms. The garbage collector manages reachable managed objects; native resources follow the library's disposal and ownership rules. This guide shows you how to confirm where the memory goes, correct common ownership problems, and set up a long-run test that checks the fix before deployment.
The same discipline applies whether your vision code runs on a desktop PC or on a dedicated vision controller. The diagnostic approach is general, but handle ownership, copying, and finalization behavior depend on your library and version.
Why managed wrappers can retain native memory
A native vision library may hand C# a wrapper object: a small managed instance holding a handle to memory allocated outside the managed heap. As an illustrative example, the wrapper might occupy approximately 100 bytes while the image buffer occupies 5 MB or more. Three distinctions help explain the symptom.
First, managed heap size does not necessarily represent the native memory associated with those wrappers. Some libraries report native allocation pressure to the runtime; others use different resource-management strategies. Monitor both managed memory and process private bytes rather than relying on either measurement alone.
Second, a wrapper may have a finalizer or another fallback cleanup mechanism, but its timing is not deterministic. Finalizable objects can remain allocated while awaiting cleanup. A growing queue or another live reference can also prevent collection entirely.
Third, Dispose is the documented deterministic release mechanism for many owned wrappers. It may release a resource or decrement a reference count rather than immediately return physical memory to the operating system. Follow the library's ownership contract on every exit path, including exceptions.
Confirming the leak before you fix it
Measure, do not guess. Log your process's private bytes, managed heap size, completed inspection count, and queue depth. Run a fixed workload and compare measurements after warm-up. A repeated increase of roughly 100 KB per cycle is evidence of retention, but the size alone does not identify a native handle leak.
Then bisect. Disable the image-processing calls while keeping acquisition and correct buffer release running. If growth continues, investigate acquisition, queues, callbacks, and drivers. If growth stops, add the processing calls back one at a time until the increase reappears. Keep the input rate and queue limits consistent during each comparison.
If the investigation points to the camera driver or USB stack rather than processing, the approach changes. The articles on depth camera black image fixes for USB bandwidth and power and USB3 camera dark image problems in ROS tools cover related bandwidth and power checks. Transport faults alone do not prove that memory is leaking.
Fix 1: Dispose every owned local handle, on every path
A common retention pattern is an owned local result that never reaches its cleanup call. The following library-style example illustrates that omission; check your installed library's documentation for the exact output ownership contract.
// No deterministic cleanup for the owned output
HObject edges;
HOperatorSet.ThresholdSubPix(reducedImage, out edges, 180);
Process(edges);
// Missing Dispose for edges
Corrected, with exception safety:
HObject edges = null;
try
{
HOperatorSet.ThresholdSubPix(reducedImage, out edges, 180);
Process(edges);
}
finally
{
edges?.Dispose();
}
Here, reducedImage is a borrowed input and its owner remains responsible for its lifetime. If the current scope also owns that input, give it its own exception-safe cleanup. Disposing an input merely because it appears in a processing call can invalidate another component's handle.
The finally block matters more than it looks. If Process throws once per thousand cycles, cleanup placed only after that call is skipped on every fault. Wrap owned per-cycle resources the same way, and audit every output parameter. An out parameter does not automatically mean a new native allocation: the documented contract determines whether you own the result.
Fix 2: Ownership rules for shared and global handles
Local handles are easier to audit. Problems often survive in shared state: a global current-image field that the acquisition thread updates while the processing thread reads it. Two rules make this safer.
Rule one: distinguish assignment, ownership transfer, and copying. The statement liveImage = newImage; only assigns a reference. It can be valid when ownership transfers explicitly and the producer stops using or disposing that object. It is unsafe when both sides assume independent ownership.
There are three common designs:
- Transfer an owned frame through a bounded queue, with the consumer responsible for releasing it.
- Acquire an independent owned handle through the library's documented retain or clone operation.
- Keep a shared handle behind synchronization, with access and disposal controlled by the same owner.
A method with “copy” in its name may duplicate a handle without copying pixels. That can provide independent handle lifetimes while leaving the underlying image data shared. Use the documented deep-copy operation when either thread must modify pixels independently.
Synchronization matters whichever design you choose. A reader must not access the previous image while a writer disposes it. Protect handle acquisition and replacement with the same synchronization mechanism, or transfer ownership through a queue. Do not return an unprotected shared reference from a getter and assume it remains valid afterward.
Rule two: exactly one disposal responsibility per owned handle. Decide whether the producer or consumer releases each handle, record that decision at the declaration or interface, and enforce it. Distinct retained handles may reference the same underlying allocation; their release behavior remains library-specific.
For deeper coverage at the SDK level, see C# camera memory leaks and image buffer ownership and camera feature access memory leak troubleshooting. When processing completes asynchronously, also review matching vision results to frames with timestamps so ownership and frame identity remain aligned.
Fix 3: Forced collection as a diagnostic, not a strategy
With owned handles released correctly, a garbage-collector call per cycle is usually unnecessary overhead. Forced collection has a narrower role during diagnosis, and it cannot replace ownership tracking.
During an isolated debugging run, GC.Collect(); GC.WaitForPendingFinalizers(); can help distinguish resources awaiting finalization from resources that remain referenced. If memory falls afterward, delayed cleanup is one possibility, not proof of a particular missing Dispose. If it does not fall, investigate retained objects, native allocations, queues, pools, and allocator behavior.
These calls can introduce substantial pauses, and waiting for finalizers can block if cleanup itself stalls. Do not run this experiment on a timing-critical production path. Compare equivalent workloads before and after collection, and use allocation snapshots or native-resource diagnostics to identify the actual owner.
Do not add an hourly collection timer merely to hide unexplained growth. A collector pass cannot release resources whose wrappers remain reachable, and an allocation without a finalization mechanism will not be repaired by collection. Any production use of explicit collection needs a measured reason and an acceptable pause budget.
The same queue-lifetime discipline applies beyond vision code. The guide to preventing queue memory leaks in industrial applications discusses related producer-consumer loop problems.
Proving the fix with a soak test
Fix, then verify. Run the real cycle end to end in a controlled test environment for at least four hours, and log private bytes every ten seconds. Also record managed heap size, queue depth, completed cycles, and errors. Four hours is an initial screening period, not a guarantee for an unattended weekend run.
Use explicit acceptance criteria:
- Allow an initial warm-up period for caches, pools, and runtime initialization, then identify a repeatable operating baseline.
- Compare average private bytes over minutes 30-40 with minutes 220-230 under the same workload. Investigate a sustained increase beyond the application's measured variation.
- Confirm that managed collections and frame queues remain within documented limits throughout the test.
- In the offline test environment, inject a hundred processing exceptions and verify that owned resources still reach cleanup.
- Exercise stop, restart, reconnect, and configuration-change paths, because cleanup defects may occur outside the normal inspection cycle.
For example, a sustained retention rate of 10 KB per cycle at two cycles per second projects to 72 MB per hour in decimal units. Over 48 hours, that becomes about 3.5 GB. Even a small residual slope deserves investigation before a long unattended deployment.
If drift remains, repeat the bisection from the confirming step. Compare allocation counts and retained-object paths rather than demanding that every freed buffer immediately reduce private bytes. Allocators can retain released memory for reuse without leaking it.
When the usual answer is wrong
Dispose discipline addresses owned disposable resources, but several other conditions show similar symptoms and need different treatment:
- Queue growth. If acquisition outpaces processing, pending buffers can accumulate. Log queue depth and frame age, then bound the queue and choose an explicit overload policy. Dropped-frame counters can help, but their absence does not rule out backlog. The recovery pattern is covered in camera acquisition stalls under load.
- A leak inside the library or driver. If a minimal program with documented ownership and bounded queues still grows, report the reproducer to the vendor. Include versions, input data, allocation measurements, and the exact operation involved. A restart may provide temporary containment only during an approved idle state with acquisition stopped and machine coordination intact.
- Growing managed collections. A results list that is appended to and never trimmed remains reachable and cannot be collected. Inspect retention paths, then use a bounded history or ring buffer where appropriate.
- Caches and allocator retention. Private bytes can remain elevated after cleanup because memory is reserved for reuse. A stable plateau under repeated identical workloads differs from an unbounded increase.
Hardware sizing after the leak is fixed
When the image pipeline genuinely needs more headroom, size memory around its measured working set rather than using capacity to conceal retention. Devices in the smart cameras and vision sensors category can move supported processing onto the device, reducing host-side image transfers for suitable inspections. They still have internal buffers and require workload validation.
For multi-camera cells, account for acquisition buffers, queued frames, intermediate images, display copies, and results storage. As a simple estimate, ten simultaneous buffers of 5 MB require 50 MB before processing intermediates and runtime overhead. A documented queue cap makes this budget predictable.
The MV-VC3301H-128G66 and MV-VC5525-256G70-NN are vision-controller options to compare after profiling the workload.
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
Validate operating-system, runtime, library, and licensing compatibility before selection. Additional memory provides working-set headroom, not permission to skip cleanup or leave queues unbounded.
Pre-deployment checklist
- Every owned disposable handle has cleanup on every exit path, using
finallyor an appropriateusingscope. - Borrowed inputs are not disposed by consumers unless the interface explicitly transfers ownership.
- Shared image state has documented ownership, synchronized access, and defined pixel-copy semantics.
- Acquisition and processing queues have capacity limits and an explicit overload policy.
- Forced collection is absent from the inspection loop unless measurements justify a specific use.
- The initial four-hour soak test shows no unexplained sustained growth after warm-up, including during injected faults.
- Queue depth, frame age, dropped-frame counters, and memory measurements are reviewed together.
- If growth traces to acquisition, work through the related checks in depth camera black image: fix USB bandwidth and power issues.
Next step
If you are sizing or replacing the controller that runs the pipeline, compare the vision controllers against your measured working set, camera count, and cycle rate. Start with the system configurator, or contact us with your memory profile and ownership findings for help reviewing the requirements.
Questions
Why does Dispose help when C# has a garbage collector?
The garbage collector manages managed objects, while a wrapper may also own native resources. Dispose releases those resources according to the library's contract without waiting for collection or finalization. Use it for handles you own; borrowed handles and shared buffers require the ownership rules documented by the library.
Is calling GC.Collect on every cycle bad practice?
Usually, yes: explicit collection can add pauses without resolving the underlying retention. It cannot collect objects that remain reachable or repair unmanaged allocations without a cleanup mechanism. Use controlled collection experiments during diagnosis, then fix ownership, disposal, or queue growth rather than adding a per-cycle collection call.
How much memory growth per hour is normal for a vision application?
There is no universal acceptable hourly growth rate. Establish a baseline after warm-up and check whether repeated identical workloads reach a stable plateau. Retaining 100 KB per cycle at one cycle per second projects to 360 MB per hour in decimal units, but measurements must distinguish leaks from bounded caches and queues.
Can hardware cause what looks like a memory leak?
Acquisition problems, driver defects, and processing backlogs can resemble a library leak. Keep acquisition running with processing disabled, while still releasing buffers correctly, and compare memory, queue depth, and frame counters. A hardware change alone does not identify the cause; use a minimal reproducer to separate transport, driver, and application behavior.

