DALL·E, SDXL, and Flux for Catalogs: Comparing Image Models Through One API

A catalog image is not successful because it is merely attractive. It is successful when the generated image preserves the product’s color, material, shape, count, and commercial constraints while arriving quickly enough and cheaply enough for the workload. This article shows how to compare DALL·E, SDXL, and Flux through one application contract, then build a retryable Go client and a reproducible test plan without pretending that an unrun benchmark has already produced winners.
The practical question is not “Which model is best?” It is “Which model is best for this catalog operation, under this quality threshold, latency target, budget, and queue shape?” That distinction matters because a model can produce an impressive hero image and still be a poor choice for a high-volume product refresh.
The examples below are provider-neutral. Model names identify families or deployments; they do not guarantee one fixed endpoint, request schema, output format, availability, or price. Confirm those details in the documentation for the provider you actually select. No credentials, customer data, prices, or benchmark results are included here.
What the comparison must measure
Catalog work usually has at least two different image-generation jobs:
- Text-to-image: create a product scene from a structured description.
- Image editing or transformation: preserve a supplied product while changing its background, composition, lighting, or context.
These jobs should not be mixed into one score. If an original product photo already exists, preserving the product may matter more than the model’s ability to invent a plausible object from text. If no photograph exists, attribute fidelity in text-to-image becomes the primary risk.
Use the following dimensions as separate measurements before combining them into a decision:
| Dimension | Measurement | Decision question |
|---|---|---|
| Attribute accuracy | Pass rate for color, material, dimensions, count, shape, visible controls, and text | Does the image represent the requested product rather than a nearby idea? |
| Latency | Time to first response, time to usable image, p50, p95, and timeout rate | Can the system meet the user-facing or batch-processing deadline? |
| Cost | Provider charge per successful image plus retries, storage, moderation, and review | What does an accepted image actually cost? |
| Workload behavior | Throughput, queue time, rate-limit responses, 5xx responses, and recovery time | Does the model remain usable during bursts and backfills? |
| Operational fit | Input support, output format, aspect ratios, regional availability, and retention controls | Can the surrounding application use the result safely and consistently? |
Do not replace these measurements with a single visual preference vote. A visual review is useful, but it should be tied to a written rubric and recorded per image.
A concrete catalog case
Consider a fictional catalog team preparing images for a furniture store. The team has structured product data such as:
{
"sku": "example-chair-001",
"category": "dining chair",
"material": "matte black metal frame with natural oak seat",
"color": "black and light oak",
"quantity": 1,
"required_view": "three-quarter front view",
"background": "warm neutral studio background",
"prohibited_changes": [
"no armrests",
"no additional chairs",
"no visible brand text"
]
}
This is an example record, not a customer fact. The test harness should turn it into a prompt using a stable template and should preserve the original structured attributes alongside every generated result.
The test is successful only if reviewers can answer questions such as:
- Is there exactly one chair?
- Is the frame black rather than dark gray or chrome?
- Is the seat visibly wood rather than plastic?
- Are armrests absent?
- Is the requested view present?
- Did the model add text, logos, or decorative objects?
These questions are more useful than “Does this look good?” because they map directly to product-data integrity.
One application contract for several image models
The application should not contain model-specific branching throughout its business logic. Define one normalized request and let each adapter translate that request into the provider’s API.
The first mention of idempotency here means that retrying one logical generation job should not silently create multiple independent jobs. A provider may support an idempotency key, or it may not. When it does not, your own job table still needs a unique logical key and an atomic state transition.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strings"
"time"
)
type GenerateRequest struct {
Model string
Prompt string
Negative string
Width int
Height int
JobID string
SourceImage []byte
}
type GenerateResponse struct {
Provider string
Model string
RequestID string
ImageURL string
ImageData []byte
}
type Provider interface {
Name() string
Generate(context.Context, GenerateRequest) (GenerateResponse, error)
}
type RetryClient struct {
Client *http.Client
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
}
func (c RetryClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
client := c.Client
if client == nil {
client = http.DefaultClient
}
maxAttempts := c.MaxAttempts
if maxAttempts < 1 {
maxAttempts = 1
}
baseDelay := c.BaseDelay
if baseDelay <= 0 {
baseDelay = 250 * time.Millisecond
}
maxDelay := c.MaxDelay
if maxDelay <= 0 {
maxDelay = 5 * time.Second
}
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
if attempt > 0 {
delay := baseDelay * time.Duration(1<<(attempt-1))
if delay > maxDelay {
delay = maxDelay
}
jitter := time.Duration(rand.Int63n(int64(baseDelay)))
timer := time.NewTimer(delay + jitter)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
cloned := req.Clone(ctx)
resp, err := client.Do(cloned)
if err != nil {
lastErr = err
if ctx.Err() != nil {
return nil, ctx.Err()
}
continue
}
if !retryable(resp.StatusCode) {
return resp, nil
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("retryable HTTP status: %s", resp.Status)
if retryAfter := parseRetryAfter(resp); retryAfter > 0 {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(retryAfter):
}
}
}
return nil, lastErr
}
func retryable(status int) bool {
switch status {
case http.StatusRequestTimeout,
http.StatusTooManyRequests,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
return true
default:
return false
}
}
func parseRetryAfter(resp *http.Response) time.Duration {
value := resp.Header.Get("Retry-After")
if value == "" {
return 0
}
var seconds int
if _, err := fmt.Sscanf(value, "%d", &seconds); err != nil || seconds < 0 {
return 0
}
return time.Duration(seconds) * time.Second
}
func idempotencyKey(jobID string) (string, error) {
jobID = strings.TrimSpace(jobID)
if jobID == "" {
return "", errors.New("job id is required")
}
return "catalog-" + jobID, nil
}
type GenericProvider struct {
NameValue string
BaseURL string
Token string
Client RetryClient
}
func (p GenericProvider) Name() string {
return p.NameValue
}
func (p GenericProvider) Generate(ctx context.Context, in GenerateRequest) (GenerateResponse, error) {
key, err := idempotencyKey(in.JobID)
if err != nil {
return GenerateResponse{}, err
}
payload := map[string]any{
"model": in.Model,
"prompt": in.Prompt,
"width": in.Width,
"height": in.Height,
}
if in.Negative != "" {
payload["negative_prompt"] = in.Negative
}
body, err := json.Marshal(payload)
if err != nil {
return GenerateResponse{}, err
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.BaseURL,
bytes.NewReader(body),
)
if err != nil {
return GenerateResponse{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+p.Token)
req.Header.Set("Idempotency-Key", key)
resp, err := p.Client.Do(ctx, req)
if err != nil {
return GenerateResponse{}, err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
return GenerateResponse{}, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return GenerateResponse{}, fmt.Errorf(
"provider %s returned %s: %s",
p.NameValue,
resp.Status,
strings.TrimSpace(string(responseBody)),
)
}
var decoded struct {
ID string `json:"id"`
ImageURL string `json:"image_url"`
}
if err := json.Unmarshal(responseBody, &decoded); err != nil {
return GenerateResponse{}, err
}
return GenerateResponse{
Provider: p.NameValue,
Model: in.Model,
RequestID: decoded.ID,
ImageURL: decoded.ImageURL,
}, nil
}
func main() {
token := os.Getenv("IMAGE_API_TOKEN")
if token == "" {
panic("IMAGE_API_TOKEN is not set")
}
provider := GenericProvider{
NameValue: "example-provider",
BaseURL: os.Getenv("IMAGE_API_URL"),
Token: token,
Client: RetryClient{
MaxAttempts: 4,
BaseDelay: 250 * time.Millisecond,
MaxDelay: 4 * time.Second,
},
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
result, err := provider.Generate(ctx, GenerateRequest{
Model: "model-name-configured-by-provider",
Prompt: "One dining chair, matte black metal frame, natural oak seat, three-quarter front view, warm neutral studio background.",
Width: 1024,
Height: 1024,
JobID: "example-chair-001-run-0001",
})
if err != nil {
panic(err)
}
fmt.Printf("%s %s %s\n", result.Provider, result.Model, result.ImageURL)
}
This is a complete small client skeleton, but it is intentionally not a drop-in client for every service. The generic JSON fields, endpoint, authentication header, image input format, response shape, and idempotency behavior must be adapted to the selected provider. Treat those adaptations as explicit code, not hidden assumptions.
Retry policy: what is safe to repeat?
A retry is an operational decision, not a universal response to an error. Retry transient transport failures, request timeouts, rate limits, and selected gateway or service-unavailable responses when the provider’s documentation permits it. Do not automatically retry malformed requests, invalid authentication, policy rejections, or unsupported parameters.
The client above uses exponential backoff with jitter. In production, add these controls:
- Respect a provider’s
Retry-Aftervalue when present. - Set a total request deadline with
context.WithTimeout. - Cap the number of attempts and maximum delay.
- Record the attempt number, status code, request ID, and final outcome.
- Close response bodies before another attempt.
- Do not log bearer tokens, raw uploaded images, or sensitive prompts.
At the job level, store a durable record such as job_id, model, prompt_hash, attempt_count, provider_request_id, status, and timestamps. A network timeout does not prove that the provider did not create the image. Before creating a second job, reconcile the first request when the provider offers a lookup operation or webhook.
Adapter design for DALL·E, SDXL, and Flux
The names in the comparison should be represented as configuration, not as business logic:
models:
dalle:
provider: provider-a
model: configured-dalle-model
endpoint: https://example.invalid/images
sdxl:
provider: provider-b
model: configured-sdxl-model
endpoint: https://example.invalid/images
flux:
provider: provider-c
model: configured-flux-model
endpoint: https://example.invalid/images
The URLs above are placeholders and must not be called. The purpose of the configuration is to make the test matrix explicit and to keep model selection outside the catalog workflow.
Each adapter should normalize at least:
- the model identifier;
- prompt and optional negative prompt;
- source image and edit mode, if supported;
- width, height, and aspect ratio;
- seed or determinism controls, if available;
- output URL or downloaded bytes;
- provider request ID;
- usage and billing metadata, if returned.
Do not force unsupported features into the common contract. If one adapter supports image editing and another does not, represent that capability explicitly and exclude the unsupported path from that particular comparison instead of silently changing the task.
Build a reproducible test set
A reproducible comparison begins with a versioned input set. Put the test cases in a repository as JSON or CSV, with no credentials or personal data:
cases/
furniture-001.json
apparel-001.json
appliance-001.json
typography-001.json
prompts/
catalog-v1.txt
runs/
README.md
Each case should contain a stable identifier, structured attributes, the requested operation, allowed transformations, prohibited changes, output dimensions, and the rubric used by reviewers. Avoid changing the prompt while a run is in progress. If the prompt changes, create a new prompt version.
A useful case mix contains both easy and difficult examples:
- simple products with one dominant material;
- products with several colors or materials;
- small parts whose count matters;
- reflective, transparent, or textured surfaces;
- products with required text or labels;
- products where an edit must preserve a supplied silhouette.
Do not select only visually attractive products. The hard cases reveal whether the system needs human review, multiple attempts, a different model, or a different workflow entirely.
Run the matrix
For every case, run every model under the same prompt version, output size, input image, timeout policy, and concurrency level. Use a fixed random seed only if the API supports it and documents its meaning. A seed is not a guarantee of byte-identical output across provider versions, hardware, or deployments, so record the complete request and environment metadata.
A minimal matrix might be:
| Factor | Example levels |
|---|---|
| Model | DALL·E deployment, SDXL deployment, Flux deployment |
| Operation | Text-to-image, image edit |
| Prompt version | catalog-v1 |
| Output | One documented square size and one documented landscape size |
| Repetitions | Several independent requests per case and model |
| Load | Serial run and controlled concurrent run |
The exact number of repetitions is a design choice that depends on budget and variance. State it before the run. If the run is too small to support a stable conclusion, report it as a pilot rather than as a benchmark.
Measure latency and workload behavior
Record timestamps at the client boundary:
request_started_at
first_byte_at
response_received_at
image_download_started_at
image_available_at
attempt_number
http_status
provider_request_id
queue_or_poll_duration
total_bytes
final_status
Calculate latency from the same clock and define the endpoint clearly. “Latency” can mean time to HTTP response, time to a hosted image URL, or time until the image has been downloaded and validated. Pick one primary definition and keep the others as separate fields.
For workload testing, start with a serial run to establish a baseline. Then increase concurrency in controlled steps while recording:
- completed images per minute;
- p50 and p95 time to usable image;
- 429, 5xx, timeout, and client-error counts;
- retry count per successful image;
- queue time, if the service exposes it;
- the point at which the error rate or p95 becomes unacceptable.
Do not call the highest observed throughput the sustainable throughput. The sustainable value is bounded by the error and latency targets you define before the test.
Calculate cost per accepted image
Provider price lists and billing rules change, and different deployments may meter different units. This article therefore provides no price claim. Put the current price for the selected deployment into a versioned run configuration and capture the source and retrieval date in your internal test record.
The useful metric is not the nominal cost of one request. It is the cost per accepted image:
accepted_image_cost =
(generation_cost
+ retry_cost
+ image_download_cost
+ storage_cost
+ moderation_cost
+ human_review_cost)
/ accepted_images
If human review is not monetized in your organization, keep it as a separate operational metric rather than setting it to zero. A model that needs more manual correction can be more expensive even when its API charge is lower.
Record failed and rejected outputs. Otherwise a model that produces many unusable images can appear artificially cheap because only successful requests are counted.
Score attribute accuracy with a written rubric
Use binary or ordinal fields that reviewers can apply consistently. For example:
| Field | Score | Pass rule |
|---|---|---|
| Object count | 0 or 1 | Exactly the requested number is visible |
| Primary color | 0, 1, or 2 | 2 means correct; 1 means acceptable ambiguity; 0 means wrong |
| Material | 0, 1, or 2 | 2 means visually consistent with the structured record |
| Shape and parts | 0 or 1 | No prohibited structural change |
| Text and markings | 0 or 1 | Required text is legible or prohibited text is absent |
| Composition | 0 or 1 | Requested view and background are present |
Define a product-level pass rule before reviewing results. For example, a product might pass only when count, color, material, and prohibited-change checks all pass. This prevents a high average score from hiding a critical failure such as an extra product or a changed product color.
Use two reviewers for a subset of the images if possible. Record disagreements and resolve them using the rubric. Do not use a vision-language model as the sole judge for a commercial catalog without validating its decisions against human review.
Decision rules
Choose thresholds before looking at the final comparison. A practical decision table could look like this:
| Requirement | Example rule | Action when missed |
|---|---|---|
| Attribute accuracy | Must exceed the catalog’s acceptance threshold | Reject the model for that workflow or add review |
| p95 latency | Must remain under the user-facing or batch deadline | Reduce concurrency, change model, or move to asynchronous processing |
| Accepted-image cost | Must fit the operation’s budget | Change output size, retry policy, review policy, or model |
| Workload reliability | Must meet error and recovery targets at planned concurrency | Use queueing, backpressure, fallback, or another deployment |
A model that fails a hard attribute rule should not win because it is faster. A model that passes quality but misses the workload deadline may still be useful for overnight batch generation. The result can be a routing policy rather than one global winner.
Example routing policy
After testing, an application might implement a policy like this:
- Use the model with the highest measured attribute pass rate for products with strict preservation requirements.
- Use a lower-cost or faster model for low-risk lifestyle images when its quality threshold is met.
- Route text-heavy or label-sensitive cases to a workflow with explicit review.
- Use asynchronous queueing for batch jobs and reserve synchronous calls for interactive previews.
- Fall back only between adapters with compatible output and review semantics.
This policy is an example, not a claim about the capabilities of any named model. The actual routing decision must come from your recorded test data.
Verification checklist
Before treating a run as reproducible, verify the following:
- The test cases, prompt template, model configuration, and client version are stored together.
- Every request has a stable logical job ID.
- Secrets come from the environment or a secret manager, not from source files.
- Request and response bodies are redacted before logs leave the test environment.
- Retries are limited and distinguish transient from permanent errors.
- Each output has provider, model, request ID, timestamps, attempt count, and final status.
- The cost configuration records the deployment and price source used for the run.
- Reviewers use the same rubric and know which attributes are hard failures.
- Results separate text-to-image from image editing.
- Any missing provider feature is marked as unsupported, not silently simulated.
Failure cases to test deliberately
A happy-path run is not enough. Add controlled tests for:
- an expired or missing token;
- an invalid model identifier;
- an unsupported image size or aspect ratio;
- a malformed source image;
- a request timeout after the provider may have accepted the job;
- a 429 response with and without
Retry-After; - a 5xx response during a retry sequence;
- a response with an invalid or missing image URL;
- a duplicate job submission using the same idempotency key;
- a provider response that succeeds but cannot be downloaded;
- an application restart between submission and result reconciliation.
For each case, define the expected state transition. For example, a timeout should not immediately mark the logical job as safe to duplicate; it should move to a reconciliation state or a bounded retry state.
Limitations
This article does not provide a benchmark result, a universal model ranking, or current provider pricing. It also does not verify any specific provider’s current endpoint, retention policy, rate limit, or idempotency implementation. Those facts are deployment-specific and must be checked before production use.
Image quality is not fully captured by a numerical rubric. Reviewers can disagree, and a small test set can overrepresent easy products. Repeated generations may also vary even when the request is unchanged. Treat the output as evidence for a defined workload, not as a permanent property of a model family.
Finally, a unified API does not make providers interchangeable. Differences in safety policy, input support, output licensing, regional availability, data handling, and failure semantics remain part of the engineering decision. Keep those differences visible in the adapter and in the test report.
Conclusion
The reliable way to compare DALL·E, SDXL, and Flux for catalogs is to make the comparison operationally concrete: one normalized request, one versioned test set, one written attribute rubric, consistent latency measurements, accepted-image cost, controlled concurrency, and explicit retry and idempotency behavior.
Do not ask which model looks best in isolation. Ask which model meets the catalog’s hard quality rules at the required latency and accepted-image cost, and what happens when the provider is slow, rate-limited, unavailable, or ambiguous about whether a request succeeded. The answer may be one model, or it may be a routing policy backed by evidence from your own workload.
← Browse the practical guides · Open the glossary