Guide

Run jobs in parallel

The short version. Every PixelAPI image endpoint is asynchronous: the request returns an id in well under a second and the work happens on our GPU fleet. If your integration submits one image, waits for it, then submits the next, total time is the sum of every job. Submit several at once and poll them together instead, and total time drops roughly by the number of jobs you keep in flight.

What a single job costs in time

Per-job processing time is set by the model and the image, not by your plan. Typical GPU time we measure in production:

EndpointTypical GPU timeNotes
POST /v1/image/remove-background5–15 sLarger or busier images take longer.
POST /v1/image/remove-object10–30 sLarge masks and high-resolution images take longer.

Jobs are picked up by a worker within milliseconds of submission when capacity is free. There is no per-plan speed tier; the lever you control is how many jobs you run at the same time.

Concurrency limits

Each plan caps how many of your jobs may be open (queued or processing) at once. Submitting beyond the cap returns HTTP 429 with the message Too many concurrent jobs; wait a few seconds and retry.

PlanConcurrent jobsRequests/minWhat that means for a 100-image Remove Background batch (~10 s each)
Free trial320~6 min
Starter1060~2 min
Pro20120~1 min
Scale50300~30 s (fleet capacity permitting)

Batch times above assume enough free GPUs at that moment; during peaks the fleet processes a few jobs per tool at once and the rest wait in your queue slot, still far ahead of one-by-one submission.

Keep headroom. Running 8–10 jobs in flight on Pro gives most of the speed-up while leaving room for other requests from your app. Requests-per-minute limits from Rate limits & plans still apply to submissions and polls together, so poll each job every 2–3 seconds, not faster.

The pattern

  1. Submit each image with POST /v1/image/<endpoint> (multipart form: image, plus mask or prompt for Remove Object). Read the id from the JSON response.
  2. Poll GET /v1/image/{id} for every open job every 2–3 seconds until status is completed or failed.
  3. Download output_url. Failed jobs are refunded automatically; error_message says why.
  4. Keep at most N jobs open (your plan's limit or less); when one finishes, submit the next.

Python

Requires pip install requests. Save as parallel_jobs.py, set PIXELAPI_KEY, then run python parallel_jobs.py remove-background a.jpg b.jpg c.jpg or python parallel_jobs.py remove-object a.jpg b.jpg --mask mask.png.

import os, sys, time, concurrent.futures as cf, requests

API = "https://api.pixelapi.dev"
KEY = os.environ["PIXELAPI_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "User-Agent": "MyApp/1.0 (parallel example)"}
MAX_IN_FLIGHT = 8          # Pro plan allows 20; leave headroom for other traffic
POLL_EVERY_S = 2.5

ROUTES = {"remove-background": "/v1/image/remove-background",
          "remove-object": "/v1/image/remove-object"}

def submit(route, image_path, mask_path=None):
    files = {"image": open(image_path, "rb")}
    if mask_path:
        files["mask"] = open(mask_path, "rb")
    r = requests.post(API + route, headers=HEADERS, files=files, timeout=60)
    if r.status_code == 429:                       # concurrency or rate limit hit: back off and retry
        time.sleep(3)
        return submit(route, image_path, mask_path)
    r.raise_for_status()
    return r.json()["id"]                          # submit returns {"id": ..., "status": "queued"}

def wait(generation_id, deadline_s=600):
    t0 = time.time()
    while time.time() - t0 < deadline_s:
        r = requests.get(f"{API}/v1/image/{generation_id}", headers=HEADERS, timeout=30)
        r.raise_for_status()
        d = r.json()
        if d["status"] == "completed":
            return d["output_url"]
        if d["status"] == "failed":
            raise RuntimeError(d.get("error_message") or "job failed")
        time.sleep(POLL_EVERY_S)
    raise TimeoutError(generation_id)

def process_one(route, image_path, mask_path):
    t0 = time.time()
    gid = submit(route, image_path, mask_path)
    url = wait(gid)
    out = os.path.splitext(os.path.basename(image_path))[0] + "_out.png"
    open(out, "wb").write(requests.get(url, timeout=60).content)
    return image_path, out, round(time.time() - t0, 1)

if __name__ == "__main__":
    tool = sys.argv[1]; args = sys.argv[2:]
    mask = args[args.index("--mask") + 1] if "--mask" in args else None
    images = [a for a in args if a != "--mask" and a != mask]
    route = ROUTES[tool]
    t_all = time.time()
    with cf.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:   # bounded parallelism
        for src, out, secs in pool.map(lambda p: process_one(route, p, mask), images):
            print(f"{src} -> {out} in {secs}s")
    print(f"{len(images)} jobs finished in {time.time() - t_all:.1f}s total")

Node.js (18+)

No dependencies. Save as parallel_jobs.mjs, set PIXELAPI_KEY, then run node parallel_jobs.mjs remove-object a.jpg b.jpg --mask mask.png.

import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const API = "https://api.pixelapi.dev";
const KEY = process.env.PIXELAPI_KEY;
const HEADERS = { Authorization: `Bearer ${KEY}`, "User-Agent": "MyApp/1.0 (parallel example)" };
const MAX_IN_FLIGHT = 8;      // Pro plan allows 20 concurrent jobs; leave headroom
const POLL_EVERY_MS = 2500;
const ROUTES = { "remove-background": "/v1/image/remove-background", "remove-object": "/v1/image/remove-object" };

async function submit(route, imagePath, maskPath) {
  const form = new FormData();
  form.append("image", new Blob([await readFile(imagePath)]), basename(imagePath));
  if (maskPath) form.append("mask", new Blob([await readFile(maskPath)]), basename(maskPath));
  const r = await fetch(API + route, { method: "POST", headers: HEADERS, body: form });
  if (r.status === 429) { await new Promise(s => setTimeout(s, 3000)); return submit(route, imagePath, maskPath); }
  if (!r.ok) throw new Error(`submit ${r.status}: ${await r.text()}`);
  return (await r.json()).id;                       // submit returns {id, status:"queued", ...}
}

async function wait(id, deadlineMs = 600000) {
  const t0 = Date.now();
  while (Date.now() - t0 < deadlineMs) {
    const r = await fetch(`${API}/v1/image/${id}`, { headers: HEADERS });
    const d = await r.json();
    if (d.status === "completed") return d.output_url;
    if (d.status === "failed") throw new Error(d.error_message || "job failed");
    await new Promise(s => setTimeout(s, POLL_EVERY_MS));
  }
  throw new Error(`timeout ${id}`);
}

async function processOne(route, imagePath, maskPath) {
  const t0 = Date.now();
  const id = await submit(route, imagePath, maskPath);
  const url = await wait(id);
  const out = basename(imagePath).replace(/\.[^.]+$/, "") + "_out.png";
  await writeFile(out, Buffer.from(await (await fetch(url)).arrayBuffer()));
  return `${imagePath} -> ${out} in ${((Date.now() - t0) / 1000).toFixed(1)}s`;
}

// bounded parallelism: at most MAX_IN_FLIGHT jobs open at once
async function runAll(route, images, maskPath) {
  const queue = [...images]; const results = [];
  const worker = async () => { while (queue.length) results.push(await processOne(route, queue.shift(), maskPath)); };
  await Promise.all(Array.from({ length: Math.min(MAX_IN_FLIGHT, images.length) }, worker));
  return results;
}

const [tool, ...args] = process.argv.slice(2);
const mi = args.indexOf("--mask"); const mask = mi >= 0 ? args[mi + 1] : null;
const images = args.filter(a => a !== "--mask" && a !== mask);
const t0 = Date.now();
for (const line of await runAll(ROUTES[tool], images, mask)) console.log(line);
console.log(`${images.length} jobs finished in ${((Date.now() - t0) / 1000).toFixed(1)}s total`);

curl

Submit several jobs in the background, then poll each id:

for f in a.jpg b.jpg c.jpg; do
  curl -s -X POST https://api.pixelapi.dev/v1/image/remove-background \
    -H "Authorization: Bearer $PIXELAPI_KEY" -H "User-Agent: MyApp/1.0" \
    -F "image=@$f" | jq -r .id
done > ids.txt

# poll until every job is done
while read id; do
  until curl -s https://api.pixelapi.dev/v1/image/$id -H "Authorization: Bearer $PIXELAPI_KEY" \
        | jq -e '.status == "completed" or .status == "failed"' >/dev/null; do sleep 3; done
  curl -s https://api.pixelapi.dev/v1/image/$id -H "Authorization: Bearer $PIXELAPI_KEY" | jq -r '.status + " " + (.output_url // .error_message)'
done < ids.txt

Webhooks instead of polling (Pro and Scale)

Configure a callback once and every finished job is POSTed to you, so a batch needs no polling loop at all:

curl -X PUT https://api.pixelapi.dev/v1/account/webhook \
  -H "Authorization: Bearer $PIXELAPI_KEY" -H "Content-Type: application/json" \
  -d '{"webhook_url": "https://example.com/pixelapi-hook"}'
# -> {"webhook_url": "...", "webhook_secret": "<keep this>", ...}

Each delivery is a JSON POST with headers X-PixelAPI-Event (job.completed or job.failed), X-PixelAPI-Delivery (unique id) and X-PixelAPI-Signature (sha256=<hex HMAC-SHA256 of the raw body using your secret>). Body fields: id, model, status, output_url, error_message, credits_used, completed_at. We retry three times (0 s, 2 s, 8 s) until your endpoint answers 2xx. Verify like this:

import hmac, hashlib
def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

Send {"webhook_url": null} to remove the callback. Free and Starter accounts can store a URL but deliveries are not sent; upgrade to Pro to enable them.

What to expect

In our own test against the production API, four Remove Object jobs submitted together finished in 14 seconds total; the same four run one after another take about 45 seconds. The gain flattens once you exceed the number of GPUs free at that moment, so 8–10 in flight is a good default for Pro.

Questions or a workload larger than the Pro limit? Write to [email protected].