Skip to main content
Lesson 11 of 12 · Image Generation via API: GPT Image

Optimize Costs

Lesson 11 of 12 Updated November 2, 2026 Developer
On this page
  1. Optimize Costs
  2. Understand Cost Structure
  3. Prototype in Low, Deliver in High
  4. Never Pay Twice for the Same Image
  5. Size Precisely
  6. Edit Instead of Regenerating
  7. Know Where the Money Goes
  8. Where to Start

Updated November 2, 2026 — Image models. gpt-image-1, the model this course was originally written for, was discontinued on October 23, 2026. gpt-image-1.5, gpt-image-1-mini, and chatgpt-image-latest will be retired on December 1, 2026. Examples now use gpt-image-2.5-sunburst, which offers the highest precision for editing; gpt-image-2.5-flare is faster for standard generation. Both models also support xhigh and max quality levels. GPT Image models always return images in base64: the response_format parameter from older DALL·E models is no longer accepted and has been removed from the code. Source: Image generation (OpenAI).

Optimize Costs

In production, image generation stops being a technical curiosity and becomes a budget line item. Any team producing a few hundred visuals a month notices this quickly. The first reaction—restricting usage—is also the least smart. This lesson covers five strategies that reduce spending without compromising the quality of your deliverables, because they target what you discard rather than what you keep.

Understand Cost Structure

Generation costs depend on three factors.

FactorImpact on Cost
Quality (low, medium, high)Direct multiplier
Resolution (1024, 1536)Larger = more expensive
Number of images (n)Linear cost (n × unit price)

These three factors multiply each other, which explains why undisciplined exploration costs an order of magnitude more than the same exploration conducted with discipline. The script below lists the five combinations you will encounter most often in production. While exact amounts appear on your OpenAI dashboard, the order of the lines already shows the scale of the trade-offs.

from openai import OpenAI

client = OpenAI()

# Compare costs: same prompt, different parameters
configs = [
    {"quality": "low", "size": "1024x1024", "label": "Low / 1024"},
    {"quality": "medium", "size": "1024x1024", "label": "Medium / 1024"},
    {"quality": "high", "size": "1024x1024", "label": "High / 1024"},
    {"quality": "medium", "size": "1536x1024", "label": "Medium / 1536"},
    {"quality": "high", "size": "1536x1024", "label": "High / 1536"},
]

print("Configuration         | Usage (estimated)")
print("-" * 50)
for config in configs:
    print(f"{config['label']:22s} | Check your OpenAI dashboard")

Prototype in Low, Deliver in High

This is by far the most cost-effective strategy, and the simplest to adopt. During the exploration phase, you are not judging the finesse of a gradient: you are judging composition, framing, and overall mood, all of which are perfectly visible in quality: low. Switching to high only makes sense for the image you actually intend to publish. The workflow below formalizes this sequence by producing five economical drafts followed by a single final render, with the added benefit of switching to landscape format (1536×1024) for the definitive version. Across five trials and one delivery, the savings cited at the end of the script hover around 60% compared to an exploration conducted entirely in high.

from openai import OpenAI
import base64

client = OpenAI()

def economical_workflow(prompt: str, output: str, n_drafts: int = 5):
    """
    Two-phase workflow:
    1. Fast drafts in low quality
    2. Final render in high quality
    """
    # Phase 1: Fast exploration
    print("Phase 1: fast drafts (low quality)")
    drafts = []
    for i in range(n_drafts):
        response = client.images.generate(
            model="gpt-image-2.5-sunburst",
            prompt=prompt,
            size="1024x1024",
            quality="low",
        )

        path = f"draft_{i+1}.png"
        data = base64.b64decode(response.data[0].b64_json)
        with open(path, "wb") as f:
            f.write(data)
        drafts.append(path)
        print(f"  Draft {i+1}: {path}")

    # Phase 2: Final render (after human selection of the best draft)
    print("\nPhase 2: final render (high quality)")
    response = client.images.generate(
        model="gpt-image-2.5-sunburst",
        prompt=prompt,
        size="1536x1024",
        quality="high",
    )

    data = base64.b64decode(response.data[0].b64_json)
    with open(output, "wb") as f:
        f.write(data)
    print(f"  Final render: {output}")

    # Summary: 5 low + 1 high instead of 5 high
    print(f"\nSummary: {n_drafts} low drafts + 1 final high")
    print("Estimated savings: ~60% compared to all-high exploration")


economical_workflow(
    "Illustration of a green tech campus with solar panels, architectural style",
    "campus_final.png"
)

Never Pay Twice for the Same Image

In a real application, the same prompt returns far more often than you might expect: a user refreshing the page, a build script re-running a generation, or an icon reused across three pages. Each of these cases is an unnecessary charge. The cache below eliminates them by calculating a SHA-256 hash of the triplet (prompt, size, quality)—the three elements that determine the billed result—and truncating that hash to sixteen characters to create a manageable filename. Note the os.path.exists check: the index might know an entry whose file has been deleted from disk, in which case the code regenerates the image rather than returning a dead path.

import hashlib
import json
import os
from openai import OpenAI
import base64

client = OpenAI()

CACHE_DIR = "image_cache"
CACHE_INDEX = os.path.join(CACHE_DIR, "index.json")

def load_cache() -> dict:
    """Loads the cache index."""
    if os.path.exists(CACHE_INDEX):
        with open(CACHE_INDEX, "r") as f:
            return json.load(f)
    return {}

def save_cache(cache: dict):
    """Saves the cache index."""
    os.makedirs(CACHE_DIR, exist_ok=True)
    with open(CACHE_INDEX, "w") as f:
        json.dump(cache, f, indent=2, ensure_ascii=False)

def generate_with_cache(
    prompt: str,
    size: str = "1024x1024",
    quality: str = "medium"
) -> str:
    """Generates an image or returns the cached result."""
    # Create a unique key based on parameters
    key_data = f"{prompt}|{size}|{quality}"
    cache_key = hashlib.sha256(key_data.encode()).hexdigest()[:16]

    cache = load_cache()

    if cache_key in cache:
        cached_path = cache[cache_key]["path"]
        if os.path.exists(cached_path):
            print(f"Cache hit: {cached_path}")
            return cached_path
        else:
            print("Cache expired, regenerating...")

    # Not in cache: generate
    response = client.images.generate(
        model="gpt-image-2.5-sunburst",
        prompt=prompt,
        size=size,
        quality=quality,
    )

    path = os.path.join(CACHE_DIR, f"{cache_key}.png")
    os.makedirs(CACHE_DIR, exist_ok=True)
    data = base64.b64decode(response.data[0].b64_json)
    with open(path, "wb") as f:
        f.write(data)

    cache[cache_key] = {
        "prompt": prompt[:100],
        "size": size,
        "quality": quality,
        "path": path
    }
    save_cache(cache)

    print(f"Generated and cached: {path}")
    return path


# First generation: API call
path1 = generate_with_cache("Minimalist logo for a tech startup, blue and white")
# Second identical call: served from cache
path2 = generate_with_cache("Minimalist logo for a tech startup, blue and white")

Size Precisely

Generating a high-resolution thumbnail that will be displayed in an 80-pixel square is like paying for an exhibition print just to slip it into a wallet. The remedy is to stop leaving this choice to the developer’s whim and instead derive it from the declared usage. The generate_for_usage dictionary splits cases into three families: small web formats—thumbnails and avatars—which are fine with low; standard web usage—blog headers, social posts—which call for medium; and media where finesse truly matters—hero banners, A4 print, portfolios—which justify high. The final two calls illustrate the gap: the same astronaut cat costs a multiple depending on whether it ends up as a thumbnail or a homepage banner.

from openai import OpenAI
import base64

client = OpenAI()

def generate_for_usage(prompt: str, usage: str, output: str):
    """Automatically chooses optimal resolution and quality."""
    configs = {
        # Small web usage
        "thumbnail": {"size": "1024x1024", "quality": "low"},
        "avatar": {"size": "1024x1024", "quality": "low"},

        # Standard web usage
        "blog_header": {"size": "1536x1024", "quality": "medium"},
        "social_post": {"size": "1024x1024", "quality": "medium"},

        # High-quality usage
        "hero_banner": {"size": "1536x1024", "quality": "high"},
        "print_a4": {"size": "1024x1536", "quality": "high"},
        "portfolio": {"size": "1024x1024", "quality": "high"},
    }

    config = configs.get(usage, {"size": "1024x1024", "quality": "medium"})

    response = client.images.generate(
        model="gpt-image-2.5-sunburst",
        prompt=prompt,
        size=config["size"],
        quality=config["quality"],
    )

    data = base64.b64decode(response.data[0].b64_json)
    with open(output, "wb") as f:
        f.write(data)
    print(f"Usage {usage} ({config['quality']}/{config['size']}) → {output}")


generate_for_usage("Cute cat in an astronaut suit", "thumbnail", "cat_thumb.png")
generate_for_usage("Cute cat in an astronaut suit", "hero_banner", "cat_hero.png")

Edit Instead of Regenerating

The client approves the photo but wants a different sky. The natural reflex—re-running the full generation with an adjusted prompt—makes you pay for an entire image and, worse, makes you lose the rest of the composition that everyone had approved. images.edit() handles this situation for the price of an edit: the edit_detail function builds a mask that frees only the relevant rectangle, here the top 300 pixels of height corresponding to the sky, and the prompt describes only the content of that strip. The landscape itself does not move a pixel.

import base64
from openai import OpenAI
from PIL import Image, ImageDraw

client = OpenAI()

# Instead of regenerating the entire image to change a detail,
# edit only the relevant area.

def edit_detail(image_path: str, zone: tuple, new_content: str, output: str):
    """
    Edits a detail in an existing image.
    zone: (x1, y1, x2, y2) of the area to modify
    """
    source = Image.open(image_path)
    mask = Image.new("RGBA", source.size, (0, 0, 0, 255))
    draw = ImageDraw.Draw(mask)
    draw.rectangle(zone, fill=(0, 0, 0, 0))
    mask.save("temp_mask.png")

    response = client.images.edit(
        model="gpt-image-2.5-sunburst",
        image=open(image_path, "rb"),
        mask=open("temp_mask.png", "rb"),
        prompt=new_content
    )

    with open(output, "wb") as f:
        f.write(base64.b64decode(response.data[0].b64_json))
    print(f"Detail edited: {output}")
    print("Cost: 1 edit instead of 1 full generation")


# Example: change just the sky of a photo
edit_detail(
    "landscape.png",
    (0, 0, 1024, 300),
    "Blue sky with some fluffy white clouds",
    "landscape_sky_edited.png"
)

Know Where the Money Goes

The four previous strategies assume you know what you are consuming, and this visibility does not come automatically: the provider’s dashboard gives you a total, not a breakdown by project or quality. A local log fills this gap for negligible development cost. log_generation adds a timestamped line for every call, truncating the prompt to 100 characters to remain readable, and usage_report then aggregates everything by quality level. This breakdown triggers decisions: if your log reveals that 80% of your images are generated in high while the majority end up on social posts, you have just found your savings pool.

import json
import os
from datetime import datetime

USAGE_FILE = "image_usage_log.json"

def log_generation(prompt: str, size: str, quality: str, n: int = 1):
    """Logs every generation to track costs."""
    if os.path.exists(USAGE_FILE):
        with open(USAGE_FILE, "r") as f:
            log = json.load(f)
    else:
        log = []

    entry = {
        "timestamp": datetime.now().isoformat(),
        "prompt": prompt[:100],
        "size": size,
        "quality": quality,
        "n_images": n,
    }

    log.append(entry)

    with open(USAGE_FILE, "w") as f:
        json.dump(log, f, indent=2, ensure_ascii=False)


def usage_report():
    """Displays a consumption report."""
    if not os.path.exists(USAGE_FILE):
        print("No consumption data.")
        return

    with open(USAGE_FILE, "r") as f:
        log = json.load(f)

    total = len(log)
    by_quality = {}
    for entry in log:
        q = entry["quality"]
        by_quality[q] = by_quality.get(q, 0) + entry["n_images"]

    print(f"Total generations: {total}")
    print(f"Breakdown by quality:")
    for q, count in sorted(by_quality.items()):
        print(f"  {q}: {count} images")


usage_report()

Where to Start

Not all these strategies offer the same return on effort, and the following table helps you prioritize them.

StrategyEstimated SavingsEffort
Low for prototyping50-70%Minimal
Prompt caching10-30% (if repetitive)Moderate
Adapted resolution20-40%Minimal
Editing vs. regenerating30-50% (per modification)Moderate
Expense trackingIndirect (visibility)Moderate

Two lines stand out clearly: prototyping in low and adapting resolution to usage require minimal effort for half the possible savings. Start with those, then add caching and targeted editing when your volume justifies it. Expense tracking itself saves nothing directly—but it is what tells you which of the other four are actually bearing fruit in your specific context.

The best way to stop thinking about it is to encapsulate these rules in a single choke point. Write a wrapper around client.images.generate() that integrates prompt caching, automatic quality and resolution adaptation based on declared usage, and logging of every call; then run it on ten generations and read the consumption report. From there, your entire team saves money without having to remember anything.