The useful choice is not the model with the prettiest demo. For a property-management web app that scores candidates against a job rubric and also generates an image from text, the choice is the runtime contract that preserves scoring quality when image work is slow, rejected, retried, or simply irrelevant to the hiring decision. I would put the candidate score on the critical path, treat the generated image as an asynchronous presentation artifact, and select among DALL-E 3, SDXL, and Flux only after measuring both against the same prompts, timeout, and acceptance rules.
A clean image cannot rescue a bad candidate score. Latency can.
What should the candidate experience wait for?
The first design question is not which image model wins. It is: what must be true before the browser can show a hiring decision? In this application, the answer is the rubric result, its evidence, and a traceable status. A generated illustration for a leasing-role landing page, a property card, or a candidate-facing explanation can arrive later. Making that image a prerequisite turns a secondary output into a hiring-system dependency.
I use two budgets. The decision budget covers rubric extraction, validation, and persistence. The media budget covers prompt submission, generation, moderation checks, storage, and delivery. They have different failure policies. A missed media deadline should leave a stable placeholder and a retriable job; a missing or malformed score should keep the decision in a pending state.
That split also makes quality measurable. For candidate scoring, quality means the rubric was applied consistently and the result can be reviewed. For generated images, quality means the output passes a small acceptance set: the prompt intent is recognizable, disallowed content is rejected, and the asset has the dimensions and format the frontend expects. A single aesthetic preference is not a production metric.
How should a property app choose an image generation model?
DALL-E 3, SDXL, and Flux are three named test arms, not three promises. A hosted model endpoint and a self-managed inference service expose different queues, retry surfaces, and operational responsibilities; even when two outputs look comparable, their tail latency and failure handling can be very different. The application should record those differences instead of hiding them behind a generate_image() helper.
The comparison belongs in a fixture-driven harness. Keep a versioned prompt set representing the property-management workflow: “quiet apartment lobby for a leasing coordinator role,” “accessible maintenance team meeting,” and a deliberately difficult prompt containing text that the UI must not depend on. Store the model identifier, request parameters, start and finish timestamps, response status, output dimensions, moderation result, and reviewer score. Do not call the fastest median the winner if its slowest requests miss the page budget.
The limitation is important: a fixture cannot predict every future prompt, and a reviewer score cannot stand in for a policy decision. The trade-off is deliberate. A small, repeatable test set gives a team a defensible release gate; a larger open-ended test set costs time and still leaves unknown inputs. Keep the gate small enough to run on every adapter change, then sample real jobs after release.
| Decision signal | What to measure | Why it matters here |
|---|---|---|
| Quality | Blind reviewer score against the same rubric | A pretty image is not evidence of a useful candidate workflow |
| Speed | p50, p95, timeout rate, and queue time | The web app needs a predictable fallback, not a demo average |
| Contract | Output format, size limits, and error classes | Storage and delivery should not discover incompatibilities late |
| Control | Audit fields, retention, and access boundaries | Candidate-related assets need deliberate handling |
This is where a storage architect becomes annoying in a productive way. The original prompt is not the image, and neither is a model response the durable record. Persist a job record with a content hash, model label, request timestamp, status, and policy decision. Store the resulting object separately. If the same request is retried, idempotency prevents a duplicate asset from becoming two different UI states.
The fixture should also preserve the uncomfortable cases. Include a prompt that asks for legible signage, because an image that looks plausible at thumbnail size may fail when the property card is opened. Include a prompt with an ambiguous role description, because the application should expose uncertainty instead of allowing an attractive scene to imply facts about a candidate. Include a request that exceeds the media deadline, then verify that the score remains readable, the placeholder is stable, and the worker can resume without changing the candidate record. These cases cost little to run but expose a boundary that aggregate quality scores hide: the image service is allowed to be incomplete, while the rubric decision must remain explainable. The test report should therefore show accepted, rejected, timed-out, and retried jobs separately. Combining them into one success percentage makes the result look tidy and makes the rollout decision worse.
How do retries change the quality-versus-speed decision?
A retry is not free time. It can produce a different image, amplify a rate-limit event, and make a user believe that the system changed its mind. HTTP 429 means the server is limiting requests, and the response may provide guidance about when to try again; a client should respect that signal and use bounded backoff rather than an immediate loop. See the MDN reference in the sources.
Three attempts is a policy boundary, not a promise that the third attempt will be useful.
The worker below is intentionally generic. It separates transient transport failures from policy failures, records an idempotency key, and gives the UI a terminal fallback. The adapter can point at any of the three test arms without changing the job state machine.
from dataclasses import dataclass
from time import sleep
from typing import Callable
@dataclass
class ImageJob:
job_id: str
prompt: str
model: str
attempts: int = 0
status: str = "queued"
def run_image_job(
job: ImageJob,
generate: Callable[[str, str, str], dict],
save_asset: Callable[[dict, str], str],
max_attempts: int = 3,
) -> ImageJob:
idempotency_key = f"property-app:{job.job_id}"
while job.attempts < max_attempts:
job.attempts += 1
job.status = "running"
try:
result = generate(job.model, job.prompt, idempotency_key)
if result.get("policy") != "accepted":
job.status = "rejected"
return job
save_asset(result, idempotency_key)
job.status = "complete"
return job
except TimeoutError:
if job.attempts == max_attempts:
job.status = "fallback"
return job
sleep(2 ** (job.attempts - 1))
except RateLimitError:
if job.attempts == max_attempts:
job.status = "fallback"
return job
sleep(2 ** job.attempts)
job.status = "fallback"
return job
The exact sleep values are policy, not a universal recipe. They must be tested against the service contract and the media budget. The important boundary is that a rejected image is not retried as though it were a network failure, and a timed-out request is not silently presented as a successful empty result.
What does production quality include beyond pixels?
For this property-management workflow, generated media should be opaque to the score calculation. The score service consumes structured candidate evidence; the media service consumes a text prompt and returns an asset state. A shared correlation ID links them for review, while separate authorization checks keep a decorative asset from becoming an accidental channel for sensitive candidate data. OWASP’s LLM guidance is a useful reminder that application boundaries, input handling, and output handling remain security concerns even when the output is an image.
Measure the whole path: prompt acceptance, queue wait, generation duration, policy review, object write, and browser delivery. Emit one event per state transition. Alert on rising p95 latency, retry volume, rejected-output rate, and jobs stuck past their deadline. A dashboard that reports only successful generations is a status page for the happy path.
There is a practical storage trap here. Do not make an object URL the source of truth for a candidate record. URLs expire, access rules change, and a later retry may create a new asset. Keep a durable asset record, reference it from the page state, and make deletion and retention explicit. The image may be optional; the audit trail is not.
A rollout that can survive the first queue spike
Start with shadow evaluation: send the fixed prompt fixture to DALL-E 3, SDXL, and Flux through separate adapters, but do not expose the generated result as part of the candidate decision. Compare blind quality reviews with latency distributions and failure categories. The result may be different for different prompt classes, which is a reason to retain the measurements, not a reason to publish a universal ranking.
Then release one narrow property-management workflow behind a feature flag. Keep the score path synchronous only where its own contract requires it; enqueue image work, show an explicit pending state, and return a deterministic placeholder on timeout or policy rejection. During the rollout, sample completed jobs for review and keep enough metadata to reproduce the selection decision without storing more candidate information than the retention policy allows.
The final decision rule is compact: choose the test arm whose accepted outputs meet the image-quality threshold while its p95, timeout behavior, storage contract, and operational load fit the media budget. If none meets that bar, remove image generation from the blocking workflow. That is a valid architecture decision, and it protects the quality of the thing the application was built to decide: the candidate rubric result.
Sources
- OWASP, “OWASP Top 10 for Large Language Model Applications”: https://owasp.org/www-project-top-10-for-large-language-model-applications/
- MDN, “HTTP 429 Too Many Requests”: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429
Top comments (0)