AWS & Cloud ← Back to blog

Parallel Image Generation with ThreadPoolExecutor and Bedrock

01 May 2026 · Matt

Generating 4 images concurrently — 1 hero + 3 blog type covers — using Python ThreadPoolExecutor and Stable Image Core.

The Problem with Sequential Generation

Each Stable Image Core invocation takes about 15 seconds. Generating 4 images sequentially would add a full minute to the deployment pipeline. Running them in parallel keeps the total time close to the time of a single image.

The Implementation

from concurrent.futures import ThreadPoolExecutor, as_completed

def generate_with_images(site) -> dict:
    content     = generate_text_only(site)
    slug        = site.slug
    hero_prompt = content.pop("hero_image_prompt", f"hero image for {site.name}")

    type_prompts = []
    for i, bt in enumerate(content.get("blog_types", [])):
        prompt = bt.pop("image_prompt", f"blog category image for {site.name}")
        type_prompts.append((f"type_{i}", prompt))

    jobs    = [("hero", hero_prompt)] + type_prompts
    results = {}

    with ThreadPoolExecutor(max_workers=4) as pool:
        futures = {
            pool.submit(_generate_image, prompt, slug, name): name
            for name, prompt in jobs
        }
        for future in as_completed(futures):
            img_name = futures[future]
            try:
                results[img_name] = future.result()
            except Exception:
                results[img_name] = None

    content["hero_image_url"] = results.get("hero")
    for i, bt in enumerate(content.get("blog_types", [])):
        bt["image_url"] = results.get(f"type_{i}")

    return content

Per-Image Upload to S3

Each image is base64-decoded from the Bedrock response and uploaded to S3 under cms-images/{site-slug}/{name}.png.

def _generate_image(prompt: str, slug: str, name: str) -> str:
    client   = boto3.client("bedrock-runtime", region_name="us-west-2", ...)
    response = client.invoke_model(
        modelId=IMAGE_MODEL,
        body=json.dumps({"prompt": prompt, "aspect_ratio": "16:9", "output_format": "png"}),
        contentType="application/json",
        accept="application/json",
    )
    body        = json.loads(response["body"].read())
    image_bytes = base64.b64decode(body["images"][0])

    key = f"cms-images/{slug}/{name}.png"
    _boto_s3().put_object(Bucket=S3_BUCKET, Key=key, Body=image_bytes, ContentType="image/png")
    return f"https://{S3_BUCKET}.s3.{S3_REGION}.amazonaws.com/{key}"
Tip: as_completed() returns futures in completion order, not submission order — faster jobs finish first. Use the futures dict to map each future back to its name so you can store results keyed by job name.