The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
If a Python loop using MSS keeps increasing memory, first look for references that keep old ScreenShot objects, NumPy arrays, Pillow images, queue entries, or converted copies alive. Reuse one MSS instance, capture only the monitor or region you need, process each frame immediately, and release or overwrite frame references before the next iteration. These changes prevent application-level accumulation, but they cannot by themselves explain every increase in process RSS or fix a platform backend defect.
What actually fills memory in an MSS loop?
MSS.grab() returns a ScreenShot object containing pixel data. A single frame can therefore remain expensive when another part of the program still references it. The usual retention paths are straightforward:
- An unbounded list such as
frames.append(sct.grab(...)). - A callback, closure, cache, or global variable that captures each frame.
- A producer queue whose consumer processes frames more slowly than the capture loop produces them.
- Several representations of the same frame, such as MSS pixels, a NumPy array, a Pillow image, and a model tensor.
- An explicit
.copy()that was added to obtain independent storage and therefore allocates another pixel buffer.
The first question is not “does MSS leak?” It is “what objects are still reachable after this iteration?” Keep only the data required for the current computation or a deliberately bounded history.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
The memory-bounded capture pattern
Create one capture object outside the loop and close it when the capture session ends. The context manager handles MSS resources; it does not destroy screenshot objects that your own code has retained.
#1 Best Overall
import mss
from mss.models import Region
region = Region(left=0, top=40, width=800, height=640)
def should_capture():
return True # replace with your stop condition
def process(frame):
# Analyze, encode, or dispatch this frame here.
# Do not append every frame to an unbounded collection.
pass
with mss.MSS() as sct:
while should_capture():
screenshot = sct.grab(region)
process(screenshot)
# On the next iteration, this name is overwritten.
The important lifecycle is one MSS instance, one needed region, one active frame, and processing before the next capture. If processing is asynchronous, transfer ownership deliberately and bound the queue rather than allowing an unlimited backlog.
Keep a bounded history when you really need one
from collections import deque
recent = deque(maxlen=30)
with mss.MSS() as sct:
while should_capture():
frame = sct.grab(region)
recent.append(frame)
# deque removes the oldest reference automatically
A bounded container limits retained frame count, although each retained frame still consumes memory until evicted. For long-term storage, encode frames to disk or another external store instead of keeping raw pixel objects in RAM.
Reuse MSS instead of constructing it per frame
MSS’s intensive-use guidance shows creating an MSS instance once around repeated captures, and notes that keeping that instance as a class attribute can be useful. Avoid this pattern:
while should_capture():
with mss.MSS() as sct:
frame = sct.grab(region)
process(frame)
Opening and closing the capture backend for every frame adds setup and teardown work and can complicate resource behavior. Use a single context-managed instance for the session. This does not release old frames that your application has placed in lists, queues, or other objects; those references still need to be removed.
Rank #2
Capture fewer pixels
MSS accepts a monitor, a region, or monitor geometry. A full desktop capture contains every pixel even when your algorithm needs only a panel, toolbar, or video rectangle. Define the smallest useful bounding box:
from mss.models import Region
# Coordinates are in screen space; adjust them for your display layout.
region = Region(left=120, top=80, width=800, height=640)
with mss.MSS() as sct:
frame = sct.grab(region)
process(frame)
Capturing fewer pixels reduces the payload that must be held and converted, but the exact saving depends on dimensions, pixel format, conversion steps, and downstream libraries. Measure your own pipeline rather than assuming a fixed percentage.
Conversions, aliases, and copies
MSS exposes pixel data through interfaces such as bgra and rgb, and can be converted for Pillow, NumPy, PyTorch, or TensorFlow workflows. A conversion may share the screenshot’s pixel memory, or it may allocate new storage; MSS documents that this depends on the implementation and environment. Treat every derived object as a possible owner or alias until you have checked the operation you use.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsPrefer one representation per stage
If OpenCV is your consumer, use the BGR form expected by that pipeline instead of repeatedly converting RGB to BGR and back. If a model accepts a NumPy array, convert once, process it, and discard the array when the result no longer needs it. Avoid retaining the MSS object, an array, a Pillow image, and a tensor for the same frame unless the workflow truly requires all four.
Use copy() only for independence
import numpy as np
with mss.MSS() as sct:
shot = sct.grab(region)
view = np.asarray(shot)
independent = view.copy() # deliberate second pixel allocation
process(independent)
copy() guarantees independent NumPy storage, which is useful when the destination must outlive or be safely modified separately from the screenshot. It also intentionally increases peak memory. If no independent lifetime or mutation is needed, keep a view and release it with the rest of the frame.
Shared memory also matters for correctness: modifying one view can affect another object that aliases the same pixels. Check ownership and writeability before mutating returned data.
Platform and version considerations
Current MSS usage documentation describes automatically exposed direct screenshot buffers on GNU/Linux with Python 3.12 or later when the supported path is available. This can avoid a separate Python-owned copy. It is an optimization for that documented environment, not a solution for code that intentionally stores old arrays or screenshots. Support for other systems is described as planned rather than universally available.
Free tools Windows power users keep installed
One-click scans. No signup required.
Project release notes describe platform-sensitive behavior, including Linux shared-memory capture with an XGetImage fallback, Windows capture implementation changes, and a macOS backend memory-leak fix. Do not attribute a rise to one of these histories without recording your installed MSS version, Python version, operating system, and display backend. Backend behavior can change between releases and fallback paths.
Queues and asynchronous processing
A queue makes capture and processing independent, but it also makes retained frames explicit. If capture produces 60 frames per second and processing consumes 30, an unbounded queue grows continuously. Choose a policy:
- Back-pressure: block capture when the queue reaches a fixed limit.
- Drop-old: retain only the newest frame when latency matters more than completeness.
- Drop-new: preserve already queued work when every accepted frame must finish.
- Encode early: replace raw pixel objects with a deliberately sized compressed payload before queuing.
Workers and display windows need the same lifecycle discipline. Stop producers before shutdown, drain or cancel workers intentionally, and clear references held by callbacks.
Diagnose growth after old frames are released
- Remove unbounded frame lists and cap every queue.
- Run a warm-up period, then compare memory while the loop is active and after processing has stopped.
- Search for references in globals, closures, caches, display buffers, worker arguments, and model pipelines.
- Record whether each stage holds an MSS object, a view, a copied array, an encoded image, or a tensor.
- Check MSS, Python, operating-system, and display-backend versions before investigating backend-specific history.
- Distinguish Python allocations from process RSS. RSS may remain high after objects become unreachable because the runtime or operating system allocator can keep arenas for reuse; a falling reference count does not require an immediate fall in RSS.
No small code change guarantees that every memory increase disappears. If the application no longer retains old frames but RSS still trends upward, isolate downstream libraries, asynchronous workers, GUI code, and caches, then profile the complete pipeline.
Common symptoms and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Memory rises one frame at a time | List, callback, or cache retains each screenshot | Process in place, remove references, or use a bounded container |
| Memory rises only when processing is slow | Producer queue grows faster than its consumer | Set a maximum size and choose a drop or back-pressure policy |
| Memory jumps after NumPy conversion | Conversion allocated storage or an explicit copy was made | Use one representation; copy only when independent storage is required |
| RSS stays high after cleanup | Allocator retention rather than live frame references | Measure live objects separately from RSS and observe later reuse |
| Growth differs by computer | OS, backend, fallback path, or library-version differences | Record versions and backend, then reproduce with a minimal loop |
Or skip the browser setup
If your goal is obtaining website images rather than capturing the local desktop, ScreenshotNeo provides a website screenshot API and MCP server for developers. It removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Each response identifies the page verdict and billing status in headers. Its MCP server lets Claude, Cursor, or another MCP client call take_screenshot, get_page_info, and capture_pdf.
One request returns PNG, JPEG, WebP, or PDF. The same service supports full-page and element captures, device presets and custom viewports, retina scale, waits, custom CSS and JavaScript, clicks, hidden selectors, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification.
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters and response handling. Equivalent Python and Node.js calls are:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await Bun.write('shot.webp', bytes);
ScreenshotNeo’s Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. Create a free ScreenshotNeo account.
Frequently Asked Questions
Should I delete the screenshot variable manually?
Usually overwriting it on the next iteration or letting the function return is sufficient, provided no other object still references the frame or a derived representation.
Does MSS always copy pixels when converting to NumPy?
No. MSS documents that conversions may share pixel memory depending on the implementation and environment, so verify ownership when independence matters.
Why can RSS remain high after cleanup?
Process RSS reflects allocator and operating-system behavior as well as live Python objects; released memory may remain reserved for reuse instead of immediately returning to the OS.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

