A preview that tears and stutters at buffer_count = 1, then runs clean at 3, is not luck: the queue between the camera and your callback finally has enough slack to cover scheduling jitter. That queue — its depth, its memory and its requeue discipline — decides whether a machine-vision stream drops frames over an eight-hour shift. This guide walks the pipeline frame by frame, sizes the pool from your frame period and worst stall, prices the memory, and separates what allocation_mode changes from what it cannot fix. It is written for integrators writing host-side acquisition code against GigE, USB3 or CoaXPress area scan cameras, whether the host is a laptop or a vision controller.
What does buffer_count do when you start a stream?
buffer_count sets how many pre-allocated frame buffers the transport layer may fill ahead of your callback, which makes it the depth of the queue between the camera and your code, and most machine-vision SDKs default to 7-10 buffers per stream.
At stream start the SDK allocates that many buffers, each sized to hold one frame payload, and announces them to the transport layer — the driver-side component that assembles pixel data from GigE packets, USB transfers or CoaXPress lanes. The camera finishes an exposure; the transport layer claims a free buffer, writes the image into it, and hands it to your application through a new-buffer event. You process the frame, you requeue the buffer, and the buffer returns to the pool for the next exposure. Two workers share one ring: the transport layer fills, your code empties.
While a free buffer exists, nothing blocks. When every buffer is in use, the arriving frame has nowhere to land: the SDK discards it, or overwrites the oldest, depending on the buffer handling mode your SDK exposes. Either way the image is gone, and the only quantity you control is how many stalls the pool can ride out.
Why did one buffer tear and stutter until three fixed it?
With buffer_count = 1 the single buffer must be processed and requeued before the next frame lands, so one 40 ms hiccup at 25 fps already loses an image, while three buffers absorb a 100 ms stall and the stutter and tearing stop.
One buffer leaves the pool no slack. The moment your callback runs long, the transport layer holds a finished frame with nowhere to put it, the SDK's drop counter climbs, and the preview stutters. Tearing is the uglier sibling: the same memory gets refilled while your display path still reads it, so one image shows the top of an old frame under the bottom of a new one. The full diagnosis of that symptom is in torn frames in a camera callback: copy the buffer before you queue it.
Three buffers change the timing budget, not your processing. At 25 fps a frame lands every 40 ms, so three buffers cover the one in your hands plus roughly two frames of stall — 80-100 ms of jitter. That rides out the pauses a desktop OS scheduler produces every few hundred milliseconds, which is why the picture ran clean at 3.
How many buffers should you set?
Set 7-10 buffers for a single camera whose processing keeps up, 15-20 when two or three cameras share one host, and 20-30 for high frame rates or multi-camera rigs, because depth must cover your longest plausible stall divided by the frame period.
Worked example, one camera: 6 MP mono, 3072 × 2048 pixels, 25 fps. The period is 1000 / 25 = 40 ms. Your worst observed stall — a logging flush colliding with a garbage-collection pause — is 120 ms. The pool needs 120 / 40 + 1 = 4 buffers. buffer_count = 3 sits exactly on that edge: it fixed the stutter and holds no margin, while ten buffers ride out a 360 ms stall.
Frame rate moves the arithmetic harder than stall length does. At 120 fps the period is 1000 / 120 = 8.3 ms, so the same 120 ms stall consumes 120 / 8.3 = 14.5, call it 15 frames, plus the one in the callback — 16 buffers before any margin. That is the whole case for 20-30 on high frame rate and multi-camera hosts.
| Case | buffer_count | Basis |
|---|---|---|
| One camera, processing keeps up | 7-10 | survives a 240-360 ms stall at 25 fps |
| Two or three cameras on one host | 15-20 | three schedulers stall the host, not one |
| Above 100 fps or four or more cameras | 20-30 | an 8.3 ms period turns a 120 ms stall into 16 buffers |
Memory is the remaining input, and the next section shows it is almost never the binding one.
How much host memory does a deep queue cost?
A deep queue costs almost nothing: ten 6 MP Mono8 buffers hold 6 MiB each and 60 MiB in total, thirty RGB8 buffers at 18 MiB each need 540 MiB, and that is a fraction of the 8 GB in a small vision controller.
The pool is allocated once, up front, so this is resident memory the host carries for the whole shift, not per-frame churn. For a 6 MP frame at 3072 × 2048:
| Pixel format | Bytes per pixel | One buffer | 10 buffers | 30 buffers |
|---|---|---|---|---|
| Mono8 | 1 | 6 MiB | 60 MiB | 180 MiB |
| Mono12 packed | 1.5 | 9 MiB | 90 MiB | 270 MiB |
| RGB8 | 3 | 18 MiB | 180 MiB | 540 MiB |
Four cameras at 30 RGB8 buffers each hold 4 × 540 MiB = 2.1 GB, comfortable in a 32 GB host. Memory therefore never argues for a shallow pool: size the queue for stalls, not to save RAM. The reuse discipline runs through the whole host, not just the pool, as reuse image buffers instead of per-frame allocation shows for every image structure your code allocates.
What does allocation_mode change under the hood?
allocation_mode chooses who owns the memory under each buffer: an announce mode such as AnnounceFrame allocates every buffer once at stream start and cycles them through the pool, while a per-frame mode allocates and releases memory as each frame arrives.
Announce-style allocation is the pattern the GenICam GenTL standard is built on: the application allocates its buffers, announces them to the transport layer before streaming, and the transport layer fills only those. The allocations happen once, the callback path contains no allocator calls at all, and each buffer keeps a fixed address — which is what makes zero-copy processing possible.
A per-frame mode asks the allocator for memory as every frame lands. In the good case that costs microseconds, far inside a 40 ms budget; in the bad case the allocator takes a slow path or the heap fragments over an eight-hour shift, and the delay lands exactly when the queue is already stressed. For a machine that runs all day, announce the buffers once and cycle them. What this looks like when it fails on a frame-grabber host is in diagnose frame grabber buffer allocation errors.
Announce modes carry one obligation: the pool is sized for the payload that existed at stream start.
When does a bigger queue not fix the drops?
No queue depth saves a stream whose average processing exceeds the frame period: at 25 fps a 55 ms frame time serves 18.2 fps against 25 fps arriving, so the backlog grows by about 6.8 frames per second and a ten-buffer queue overflows in roughly 1.5 s.
Depth spreads a stall; it cannot remove a deficit. Once every buffer sits full, drops resume at the same rate as with one buffer, no matter how deep you went. The remedies sit upstream of the pool: inspect a reduced ROI, bin or downscale, run the frame rate your processing actually supports, or hand buffers to a bounded worker queue sized by the arithmetic above. The host-side version of that worker queue — bounded, reused, never left to grow — is in Python frame queue memory leak: bound the queue and reuse frames.
Which host keeps a multi-camera queue flowing?
A four-camera station needs a host with one NIC or PoE port per camera and 20-30 buffers per stream in RAM, which is the job description of a vision controller offering 5+ LAN ports and 32 GB of memory.
Count the resources in order: one NIC per camera, or PoE ports, so GigE streams do not share a link or an interrupt budget; USB bandwidth if the cameras sit on USB3; CPU headroom so every camera's callback fits inside the combined frame periods; and the buffer memory from the table above. The camera finder filters by interface, so you can count GigE against USB3 parts before committing to a host layout.
A two-camera cell fits a compact controller. The MV-VC2140-128G40-1T runs an Intel N-series processor with 8 GB of memory and 3-4 LAN ports. That covers a small cell: two streams of 15-20 Mono8 buffers need 90-120 MiB against 8 GB of RAM.
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
A larger cell wants the deep end of every filter. The MV-VC5525-256G70-NN pairs an Intel Core i5 with 32 GB of memory, 5+ LAN ports, PoE camera ports and 8 built-in light channels. That is the deep-end layout for four or more GigE cameras, each stream carrying 20-30 buffers from the table above.
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- PoE
- PoE camera ports
- Light outputs
- 8 built-in channels
The pool logic does not care where the frames come from: it holds just as well when streaming camera module frames to a PC over its debug protocol, because something upstream always writes into buffers while your callback drains them, and only the period changes.
What should you check before you touch buffer_count?
Measure before you resize: read the real frame rate from frame timestamps, read the SDK's dropped-frame counter before and after the change, and confirm the callback requeues each buffer exactly once on every path, because a mis-requeued pool fails at any depth.
- Measure the true period from frame timestamps rather than the configured frame rate; the method is in camera frame rate measurement from timestamped frames.
- Fix exposure before the stream starts so the period is known and stable; the ordering is in set manual exposure before streaming: initialization order.
- Requeue every delivered buffer exactly once, on every code path including exceptions. A buffer requeued twice surfaces as transport errors or a lost connection; a buffer never requeued shrinks the pool permanently.
- Inject a deliberate stall to prove the sizing: sleep 150 ms inside the callback at 25 fps with buffer_count = 10. The pool needs 150 / 40 + 1 = 4.75, call it 5 buffers, so expect zero drops. Raise the sleep to 450 ms and frames accumulate every 40 ms — eleven arrivals plus the one in your hands is 12 needed against 10 available, so the drop counter should report exactly two frames.
- Re-run the stall test after any ROI or pixel-format change, because the payload, and with it the buffer size, changed under the pool.
Get the pool right and the stream runs to the numbers: 7-10 buffers on one camera, 20-30 on a multi-camera host, announce-mode allocation, one requeue per frame. If you are laying out a new cell, bring the camera list, frame rates and processing budget to contact and we will check the stall arithmetic with you.
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
Questions
Does a deeper buffer queue add latency to my inspection?
In steady state, no. When the callback keeps up, most buffers sit empty and each frame reaches you within one frame period of the exposure, so latency stays near 40 ms at 25 fps. Depth only delays frames during a stall. If measured latency equals the whole queue, the pool is full and processing is the bottleneck, not the buffer count.
What happens if the callback forgets to requeue a buffer?
The pool loses that buffer permanently, because the requeue is what returns it to the transport layer. The usable queue shrinks by one each time, and after a few frames every new image is discarded. Count requeues against delivered frames in a test run: the two numbers must match exactly, once per buffer, on every code path.
Can I change buffer_count while the stream is running?
No. The pool is allocated and announced when the stream starts, so the buffer count and the buffer size are fixed for the run. Stop acquisition, release the pool, apply the new setting or payload change, re-announce, and restart. Plan for it in the application's state machine rather than as a live tweak.
Do GigE and USB3 cameras need different buffer counts?
The pool behaves identically, because both interfaces deliver frames through a transport layer that fills your announced buffers. What differs is where the stalls come from: GigE adds packet reassembly and interrupt load on the NIC, USB3 shares the host controller's bandwidth between devices. On a multi-camera host of either interface type, take the deeper end of the range.
How do I tell a torn frame from a dropped frame?
A dropped frame never arrives: you see a gap in frame IDs or timestamps and the SDK's drop counter moves. A torn frame arrives but carries part of an older image, which means the buffer was read for display while the transport layer still wrote into it. Copy the pixels out before any other code touches the buffer.

