You have a folder of a few hundred product photos, headshots or listing images, and every one needs its background removed. Doing that by hand in an editor is a day of clicking. In Python it is a loop, and the real decision is what runs inside the loop: an open-source model on your own machine, or an HTTP API.
This guide builds both, with complete scripts you can run on a folder today, and covers the parts that make a batch job reliable: model choice, CPU vs GPU, retries, rate limits, idempotency and resuming after a crash.
Option A or option B?
| Local model (rembg) | HTTP API | |
|---|---|---|
| Cost | Free software; you pay in hardware and time | Per image |
| Setup | Python 3.11 to 3.13, model downloads, optional CUDA | An API key and requests |
| Speed | Depends on your CPU or GPU and the model | Depends on the service, parallel requests |
| Quality | Depends on the model you pick | Depends on the service |
| Privacy | Images never leave your machine | Images are uploaded to a third party |
| Maintenance | You manage models, drivers and licenses | The provider does |
| Good for | Offline work, sensitive images, experiments | Production pipelines, servers without GPUs |
Many teams use both: a local model for experiments and internal tools, an API for production where predictable quality and no GPU servers matter more.
Option A: rembg, the open-source route
rembg is an MIT-licensed Python library and CLI that wraps several background removal models and runs them with ONNX Runtime. At the time of writing the current release is 2.0.86, and it requires Python 3.11 or later and earlier than 3.14.
Install
Pick the backend that matches your hardware. The CPU build works everywhere:
python -m venv .venv && source .venv/bin/activate
pip install "rembg[cpu]" # library only
pip install "rembg[cpu,cli]" # library plus the rembg commandFor NVIDIA GPUs, install rembg[gpu] instead. The project notes that this needs onnxruntime-gpu plus a matching CUDA and cuDNN, and suggests falling back to the CPU build if you cannot install those. AMD GPUs on Linux use rembg[rocm] with onnxruntime-rocm.
rembg picks the CUDA (or ROCm) execution provider automatically when ONNX Runtime reports it as available, and warns if it falls back to CPU. To check what ONNX Runtime can see:
import onnxruntime
print(onnxruntime.get_available_providers()) # look for CUDAExecutionProviderChoose a model
Models download on first use to ~/.rembg/models/ (set REMBG_HOME to change it). Some useful ones from the project's list:
| Model | Notes |
|---|---|
bria-rmbg | The default. BRIA's RMBG-2.0, about 1.02 GB, runs at 1024 x 1024. Its license requires a paid agreement for commercial use. |
birefnet-general | General-purpose BiRefNet model; good soft edges |
birefnet-general-lite | Lighter BiRefNet for general use |
birefnet-portrait | Tuned for people |
isnet-general-use | General-purpose IS-Net model |
u2net | The classic general model; faster, blockier edges |
u2netp | Lightweight u2net; quick tests on slow machines |
silueta | Same as u2net, reduced to 43 MB |
Two points matter more than they look. First, the default model is not free for commercial use: model weights carry their own licenses, separate from rembg's MIT license, so check the source of whichever model you ship with. Second, the newer models (bria-rmbg, birefnet-*, isnet-general-use) produce soft, well-shaped edges, while the older u2net family tends to give firmer, blockier edges.
Edge quality options
rembg offers three ways to treat soft edge pixels such as hair and fur:
- Default: fine for hard-edged subjects such as products, cars and logos.
decontaminate=True(-dcon the CLI): removes the colored halo that the old background leaves in edge pixels. It changes color only, never the shape of the mask, costs very little, and is safe to leave on for a whole batch.alpha_matting=True(-a): re-estimates the edge itself when the mask shape is wrong, for example strands cut off. It is much slower and can fail to converge, in which case rembg falls back to a decontaminated cut-out.
For unattended batches, the project recommends -dc over -a because it is faster and cannot fail. The alpha channel guide explains why halos happen.
The quick way: the CLI
rembg p -m birefnet-general -dc photos/ cutouts/rembg p processes a whole folder; add -w to watch the folder and process new files as they appear.
The Python script
This version loads the model once, skips files that are already done so you can stop and restart it, and keeps going when one file fails:
import sys
from pathlib import Path
from rembg import new_session, remove
EXTS = {".jpg", ".jpeg", ".png", ".webp"}
def main(in_dir: str, out_dir: str, model: str = "birefnet-general") -> None:
src_dir, dst_dir = Path(in_dir), Path(out_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
session = new_session(model) # load the model once, reuse it for every image
files = sorted(p for p in src_dir.iterdir() if p.suffix.lower() in EXTS)
for i, src in enumerate(files, 1):
dst = dst_dir / f"{src.stem}.png"
if dst.exists():
continue # resume where a previous run stopped
try:
out = remove(src.read_bytes(), session=session, decontaminate=True)
dst.write_bytes(out)
print(f"[{i}/{len(files)}] ok {src.name}")
except Exception as e: # one bad file shouldn't stop the batch
print(f"[{i}/{len(files)}] failed {src.name}: {e}")
if __name__ == "__main__":
main(*sys.argv[1:])Run it with python batch_rembg.py photos cutouts birefnet-general. Creating the session once matters: loading a model takes far longer than running it, and the rembg README recommends session reuse for batch performance. Outputs are always PNG because the result has an alpha channel.
Where rembg falls short
- First run is slow while the model downloads, and large models need a lot of RAM or VRAM.
- CPU throughput is modest with the high-quality models. Test with 20 representative images before committing to a thousand.
- Licenses and drivers are your job. Pin your rembg and model versions so a later upgrade does not silently change results.
Option B: an HTTP API
With an API, the heavy lifting happens on someone else's GPUs, and your script is mostly about handling the network well. The example uses the removebackground.dev API because its contract is published as OpenAPI 3.1 at /openapi.json; the same patterns apply to any similar service.
The contract
POST https://removebackground.dev/v1/removewith your key in thex-api-keyheader.- Send the image as a multipart field
image_file(JPG, PNG or WebP, up to 25 MB), or as JSON withimage_url. - Optional fields include
format(png,jpgorwebp),bg_color,size(preview,medium,hd,full) andcrop. - 200 means the body is the finished image. 202 means it is still processing after about 55 seconds: the body is JSON with a job
id, and you pollGET /v1/jobs/{id}/result, which answers 409not_readywithRetry-Afteruntil the image is ready. Never save a 202 body as an image. - Up to 60 processing requests a minute per account; over that you get 429
rate_limitedwithRetry-After. - An
Idempotency-Keyheader makes retries safe: the same key with the same request returns the original job instead of charging again. The same key with a different request answers 409idempotency_key_reused. - Results are deleted after 48 hours, so save them when you get them.
Retry rules that keep you from paying twice
| Response | What to do |
|---|---|
| 200 | Save the image |
| 202 | Poll the job result; do not upload again |
| 400, 401, 402, 413, 422 | Fix the request, key, credits or image; do not loop |
| 429 | Wait Retry-After seconds, resend with the same key |
| 500, 503, timeout, dropped connection | Back off and resend with the same key |
| 502 | That image failed and was not charged; log it |
The trick is to make the idempotency key deterministic: a hash of the image bytes plus the options. If the script crashes and you run it again, every resend carries the same key, so nothing is processed or charged twice.
The Python script
import hashlib
import mimetypes
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import requests
API = os.environ.get("REMOVEBG_API", "https://removebackground.dev")
KEY = os.environ["REMOVEBG_API_KEY"] # keep the key out of your code
OPTIONS = {"format": "png", "size": "full"} # or {"format": "jpg", "bg_color": "ffffff"}
WORKERS = 4 # the API allows 60 processing requests a minute
EXTS = {".jpg", ".jpeg", ".png", ".webp"}
RETRYABLE = {429, 500, 503}
def idempotency_key(data: bytes) -> str:
# Same image + same options = same key, so a rerun never pays twice.
h = hashlib.sha256(data)
h.update(repr(sorted(OPTIONS.items())).encode())
return h.hexdigest()
def wait(res, default=2.0):
try:
return float(res.headers.get("Retry-After", default))
except ValueError:
return default
def save(res, dst: Path) -> Path:
if res.status_code != 200 or not res.headers.get("Content-Type", "").startswith("image/"):
raise RuntimeError(f"HTTP {res.status_code}: {res.text[:300]}")
dst.write_bytes(res.content)
return dst
def process(src: Path, dst: Path) -> Path:
data = src.read_bytes()
mime = mimetypes.guess_type(src.name)[0] or "application/octet-stream"
headers = {"x-api-key": KEY, "Idempotency-Key": idempotency_key(data)}
for attempt in range(6):
try:
res = requests.post(
f"{API}/v1/remove",
files={"image_file": (src.name, data, mime)},
data=OPTIONS,
headers=headers,
timeout=(10, 120),
)
except (requests.ConnectionError, requests.Timeout):
time.sleep(2 ** attempt) # same key: safe to resend
continue
if res.status_code in RETRYABLE:
time.sleep(wait(res, 2 ** attempt))
continue
break
else:
raise RuntimeError("gave up after retries")
if res.status_code != 202:
return save(res, dst) # 200 is the image; 4xx and 502 raise
# 202: still processing. Poll the same job; never upload the image again.
job_id = res.json()["id"]
deadline = time.monotonic() + 600
while time.monotonic() < deadline:
time.sleep(wait(res))
try:
res = requests.get(f"{API}/v1/jobs/{job_id}/result", headers=headers, timeout=30)
except (requests.ConnectionError, requests.Timeout):
continue # reading a job is free and safe to repeat
if res.status_code == 409 and res.json()["error"]["code"] == "not_ready":
continue
if res.status_code >= 500:
continue
return save(res, dst)
raise TimeoutError(f"job {job_id} still running; fetch it later")
def main(in_dir: str, out_dir: str) -> None:
src_dir, dst_dir = Path(in_dir), Path(out_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
ext = ".jpg" if OPTIONS.get("format") == "jpg" else "." + OPTIONS.get("format", "png")
todo = [
(p, dst_dir / (p.stem + ext))
for p in sorted(src_dir.iterdir())
if p.suffix.lower() in EXTS and not (dst_dir / (p.stem + ext)).exists()
]
print(f"{len(todo)} images to process")
failed = []
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
jobs = {pool.submit(process, s, d): s for s, d in todo}
for fut in as_completed(jobs):
src = jobs[fut]
try:
print("ok ", src.name, "->", fut.result().name)
except Exception as e:
failed.append(src.name)
print("failed", src.name, e)
print(f"done: {len(todo) - len(failed)} ok, {len(failed)} failed")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])Run it with REMOVEBG_API_KEY=... python batch_api.py photos cutouts. Four workers is a modest start; if fast responses push you past the per-minute limit, the 429 handling waits and resends with the same key. To send URLs instead of files, post JSON:
res = requests.post(
f"{API}/v1/remove",
json={"image_url": "https://example.com/shoe.jpg", "format": "png"},
headers={"x-api-key": KEY, "Idempotency-Key": "shoe-front-v1"},
timeout=(10, 120),
)For long lists of URLs, the same API has POST /v1/batch, which queues up to 100 image URLs as one job that you poll.
Making any batch job robust
- Skip finished outputs so a rerun resumes instead of starting over. Both scripts do this.
- Watch for name collisions.
shoe.jpgandshoe.pngin the same folder both map toshoe.png; rename inputs or include the original extension in the output name. - Log failures to a file and re-run only those.
- Spot-check a sample at 100% zoom on dark and light backgrounds before processing everything.
- Keep originals untouched. Write to a separate output folder.
- Pick the output format for the destination. PNG or WebP for transparency, JPG on white for marketplaces. See PNG vs JPEG vs WebP.
Questions
Is rembg free for commercial use?
rembg itself is MIT-licensed, but each model has its own license. The default model, BRIA's RMBG-2.0 (bria-rmbg), requires a paid agreement for commercial use, so check the license of the model you use or pick another one.
Does rembg use my GPU automatically?
Yes, if you install rembg[gpu] and ONNX Runtime can load the CUDA provider (which needs matching CUDA and cuDNN). rembg uses CUDA or ROCm when available and warns when it falls back to the CPU. Check onnxruntime.get_available_providers().
Which rembg model gives the best quality?
The newer models, bria-rmbg (the default), the birefnet family and isnet-general-use, produce softer and better-shaped edges than the older u2net family. birefnet-portrait is a good start for people. Test a sample of your own images, since results vary by subject.
How do I avoid being charged twice when retrying API calls?
Send an Idempotency-Key header and reuse it on every retry of the same image. Deriving it from a hash of the image bytes and options means even a rerun of the whole script reuses the same keys.
What should my script do with a 202 response?
Treat it as still processing, not as an image. Read the job id from the JSON body and poll GET /v1/jobs/{id}/result, waiting for Retry-After whenever it answers 409 not_ready. Do not upload the image again.
How fast can I send requests to the removebackground.dev API?
Up to 60 processing requests a minute per account. Over that, the API answers 429 rate_limited with a Retry-After header. For many URLs, POST /v1/batch accepts up to 100 per request.
Try it on your own image
New accounts get 50 free credits. One credit per image, no subscription, and credits never expire.
Remove a backgroundSources
- rembg on GitHub (README: installation, models, edge modes)
- rembg on PyPI (version and Python requirement)
- BRIA RMBG-2.0 model card and license
- ONNX Runtime: Get started and installation matrix
- removebackground.dev API reference and guides
- removebackground.dev OpenAPI 3.1 schema
- Requests: Quickstart (multipart file uploads, timeouts)