> ## Documentation Index
> Fetch the complete documentation index at: https://docs.perceptron.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch processing

> Process independent images with bounded concurrency and a result for each item.

For a collection of independent images, run ordinary chat-completion requests through a bounded worker pool. This guide batches work in your application; it does not use a server-side batch endpoint.

If several independent questions share one image or video, consider [Multilook](/perceptron-mk1.5/guides/multilook) for that item. If one question needs several media items together, use [multiple assets](/perceptron-mk1.5/guides/multiple-assets) in a single request.

## Process a list of images

Install `perceptron>=0.4.0`, set `PERCEPTRON_API_KEY`, and create `images.txt` with one HTTP(S) image URL per line. For example:

```text theme={null}
https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/qna/studio_scene.webp
https://raw.githubusercontent.com/perceptron-ai-inc/perceptron/main/cookbook/_shared/assets/capabilities/detection/ppe_line.webp
```

Save this script as `batch_questions.py`. It submits at most four items at a time and writes one JSON record per item. The original line number identifies the input even when requests complete out of order.

```python theme={null}
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from itertools import islice
from pathlib import Path
from time import perf_counter

from perceptron import Client, SDKError, image


WORKERS = 4


def process_image(client, line_number, url):
    started = perf_counter()
    record = {"line": line_number}
    try:
        response = client.chat.completions.create(
            model="perceptron-mk1.5",
            messages=[{
                "role": "user",
                "content": [
                    image(url),
                    {"type": "text", "text": "Describe the visible objects in two sentences."},
                ],
            }],
            max_completion_tokens=512,
        )
        if not response.choices:
            raise ValueError("The response contained no completion")
        choice = response.choices[0]
        record["finish_reason"] = choice.finish_reason
        record["usage"] = response.usage.to_dict() if response.usage else None
        if choice.finish_reason != "stop" or choice.message.tool_calls:
            raise ValueError(f"Incomplete answer: {choice.finish_reason}")
        record.update(status="ok", text=choice.message.content or "")
    except (SDKError, ValueError) as error:
        record.update(status="error", error={
            "type": type(error).__name__,
            "message": str(error),
            "status_code": getattr(error, "status_code", None),
        })
    record["elapsed_seconds"] = round(perf_counter() - started, 3)
    return record


def run(path):
    client = Client(
        api_key=os.environ["PERCEPTRON_API_KEY"],
        provider="perceptron",
        timeout=60.0,
    )
    with path.open() as source, ThreadPoolExecutor(max_workers=WORKERS) as pool:
        items = ((number, line.strip()) for number, line in enumerate(source, 1) if line.strip())
        while group := list(islice(items, WORKERS)):
            futures = [pool.submit(process_image, client, number, url) for number, url in group]
            for future in as_completed(futures):
                print(json.dumps(future.result()), flush=True)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("Usage: python batch_questions.py images.txt")
    run(Path(sys.argv[1]))
```

Run it with:

```bash theme={null}
python batch_questions.py images.txt > results.jsonl
```

The script does not retry failed requests automatically. A failure on one item becomes an error record while the other items continue. An answer with `finish_reason: "length"` is recorded as incomplete, not as a successful result.

## Resume and scale deliberately

Keep the input file unchanged when using its line numbers to match saved results. For a changing dataset, assign each item a stable application ID instead. Persist completed records as you go, then select only failed or missing items for a later run. Protect the result file: answers and error messages can contain details from your inputs.

Use the [error guide](/perceptron-mk1.5/guides/error-messages) to distinguish retryable failures from invalid inputs or exhausted credits. Apply backoff and a total deadline before resubmitting an item. If you adapt the worker to a tool workflow, do not replay completed tool actions when retrying a model request.

Four workers bound concurrent requests, but do not enforce requests per minute. Pace submissions across all workers sharing your organization and reduce load when rate limited; see [scaling](/perceptron-mk1.5/guides/scaling). Compare elapsed time, completion rate, errors, and token usage on the same workload before increasing concurrency.

For local or repeatedly used media, [upload files once](/perceptron-mk1.5/guides/files) and use file references. File reuse reduces upload traffic, not the model's media-token usage.
