Skip to main content

The Presigned URL That Expired Before Your User Could Verify the Multimodal Model's Claim

· 10 min read
Tian Pan
Software Engineer

A user opens yesterday's conversation. Next to their support agent's reply sits a broken-image placeholder where their uploaded receipt used to be. The reply confidently quotes "the charge of $47.32 dated March 14 at the merchant Coffee Tribunal." The user has no way to check whether that quote is accurate, because the evidence the model worked from is now a 403 from your object store. They file a hallucination ticket. Your eval suite did not catch it because the model was, at the time of the call, exactly right.

This is a story about retention mismatch, not about model quality. Your transcript outlived its grounding. The grounding was a presigned URL with a fifteen-minute clock, and the claim about the grounding is text that will sit in your database for years. When the asset clock and the claim clock run at different speeds, every correctly-grounded multimodal answer eventually looks like fabrication to whoever revisits it.

Most teams that ship multimodal features for the first time inherit the presigned-URL pattern from their existing file-upload flow. The flow was designed for one purpose: hand a browser a short-lived link to download an image the user owns. It was never designed to be the persistent evidence backing a model's reasoning. But the easiest path to "let the model see this image" is to pass the same URL the browser already has, and the easiest path to "let the user revisit the conversation" is to embed that URL in the transcript. Both decisions are individually sensible. Together they build a system whose audit trail makes the model look like a liar after fifteen minutes.

Why Your Eval Suite Will Never Catch This

The asset-expiry failure mode is invisible to every eval design that treats a multimodal call as a single request-response transaction. Your eval suite generates a presigned URL, hands it to the model, captures the response, scores it, and discards the URL. The URL was alive for the entire test. Of course the eval passed. Of course it kept passing as you tightened the TTL from one hour to fifteen minutes for security reasons; the eval's request-to-response gap is measured in seconds.

The bug only manifests at conversation-replay time, which is a path the eval does not exercise. To catch it, you would need an eval slice that loads a stored transcript hours later and verifies that every multimodal reference still resolves. Most teams do not build that slice because the failure mode is invisible from the request-handler's point of view — the request succeeded, the response was correct, the only thing that "broke" is a piece of UI state two product surfaces away.

This is the same shape as the bug where a backend test passes against a fresh database and a production deploy passes against a fresh deploy, and the regression only shows up in a customer's six-month-old account. The signal lives in the time dimension, and request-scoped tests cannot see it. Your eval suite is measuring the wrong quantity: it scores the model's response against the image, but the user-facing problem is the divergence between the response and what the user can still see of the image. Those are different metrics, and only one of them is what your customer experiences when they file the ticket.

The Two Clocks Your System Is Running

Every multimodal product runs two clocks against the same conversation, and most teams have never named them as separate concerns.

The first clock is the asset-availability clock. It is the lifetime of the presigned URL, the object's storage policy, the CDN cache, any client-side blob URL the browser created during upload. AWS lets a presigned URL live anywhere from sixty seconds to seven days, and most security reviews push teams toward the short end of that range because a leaked URL with a longer lifetime is a longer-lived breach. Fifteen minutes is a common landing zone — long enough to survive a slow client retry, short enough to feel safe.

The second clock is the conversational-claim clock. It is however long you keep the transcript. In a consumer product that might be months. In a regulated B2B product it is typically a multi-year retention horizon driven by compliance — financial services keep broker chats for years under SEC and FINRA rules, and health products often inherit similar holds from HIPAA frameworks. The point is that the claim outlives the asset by orders of magnitude.

The architectural mistake is not running the two clocks. The mistake is conflating the URL (a transient capability) with the asset (a durable artifact) and storing the former in a place that promises the durability of the latter. When the transcript renders an <img src="..."> pointing at an expired URL, the system is telling the user "this image is permanent" while the URL is telling the browser "this image was permanent fifteen minutes ago." The contradiction is the bug.

Why This Specifically Looks Like Hallucination

Users have a remarkably consistent mental model for what hallucination looks like. The model says something specific and confident; the user cannot verify it; the user concludes the model made it up. Every part of that pattern fires when the asset is gone:

  • The model said something specific: "Coffee Tribunal, $47.32, March 14." Multimodal models tend to be more concrete than their text-only siblings because the prompt asked them to describe an image, and "describe" is a strong invitation to specificity.
  • The user cannot verify it: the image tile is broken. There is no evidence to cross-check against.
  • The user concludes the model made it up: this is the only available explanation given what the user can see. The fact that the model was correct is unrecoverable from the artifacts the user has access to.

The detection-and-mitigation literature on hallucinations recommends "make outputs auditable by requiring citations and having the model verify each claim by finding a supporting quote." But a citation is only as durable as the thing it cites. If the citation points at a presigned URL, the citation acquires the URL's expiry by inheritance. Your audit trail is only as long-lived as the shortest-lived link in it.

There is a second, subtler effect: this failure mode trains your users to distrust correct outputs. A customer who files three hallucination tickets that all turn out to be expired-asset incidents will, on the fourth occasion, stop trusting any multimodal answer that quotes specifics. The product surface has now manufactured the perception of unreliability in a system that was, by the standards of model quality, behaving correctly the whole time. That perception is hard to undo, because the only way to demonstrate the model was right is to show the user the asset, and the asset is gone.

What Closing the Gap Actually Looks Like

The fix is not "make presigned URLs live forever," which would re-open every reason the security team shortened them. The fix is to stop storing the request-time URL in the transcript and instead store a reference to the asset that the render layer can resolve into a fresh URL at view time.

The minimum viable shape of this is a per-conversation asset table that holds a stable internal identifier, a pointer to the object in storage, and any access-control metadata needed to decide whether the current viewer is allowed to see it. The transcript stores the identifier, never the URL. When the transcript renders, the render layer issues a new presigned URL against the underlying object. The URL is always fresh; the asset's own retention policy decides how long the image is actually available.

That policy is the other half of the fix. The asset's retention must be at least as long as the retention of any model output that quotes it. A receipt image cannot expire while a transcript referencing the description of that receipt is still queryable. This is not a default behavior in any object store you use; you have to enforce it explicitly, either by promoting the asset into a longer-retention bucket when the model finishes its call, or by tying the asset's TTL to the transcript's TTL as a join condition that runs at cleanup time. Either way, the deletion of the asset becomes a question of "is any claim still pointing at this?" rather than "has the upload's own clock run out?"

A subtle next step is a forensic snapshot of the image bytes alongside the model's description at request time. The reason is not the common case — for the common case, rehydrating a fresh URL against the same underlying object is enough. The reason is the case where the underlying object was replaced or modified after the model saw it. If the user uploaded version 1, the model described version 1, and the user then re-uploaded a different file at the same key, the rehydrated URL now serves version 2. The audit trail says "the model described $47.32" and the rehydrated image shows a different number. To distinguish "the asset changed under us" from "the model was wrong" you need a snapshot the model's call captured at the time it happened. This is overkill for many products and exactly the right discipline for any product whose model outputs are evidence in a downstream decision a regulator might later review.

The render layer needs a corresponding behavior for the case where the asset genuinely is gone — deleted by the user, expired by policy, or unrecoverable for any other reason. The wrong behavior is the silent broken-image tile, because that is what creates the apparent-hallucination ticket. The right behavior is an explicit "this image is no longer available; the model's description below is what it observed at request time" affordance next to the description, with a timestamp of when the asset became unavailable. This reframes the user's interpretation from "the model fabricated this" to "the evidence has been pruned." Those are very different support tickets, and only the second one is accurate.

The Architectural Realization

A multimodal model's grounding is only durable if the grounding evidence outlives the model's claim about it. This is a statement about asset retention, not about model behavior, and it lives entirely in your storage and rendering layers.

Teams that ship multimodal features confidently treat the asset, the call, and the transcript as a single durable artifact whose retention is decided at a higher level than any one component. Teams that ship multimodal features and then chase hallucination tickets for six months treat the upload URL as a request parameter and the description as a separate database row, and accept the resulting drift because no individual component is doing anything wrong.

The cleanest test of which kind of team you are is to open a conversation from six months ago in your own product and look at the multimodal turns. If the images are still there and the descriptions still match what you can see, your retention policy is coherent. If the images are broken and the descriptions are reading like a model that hallucinated specifics about pictures that do not exist, the failure mode is already in production and the support tickets are about to start. The hallucination is in your storage architecture, and the model is taking the blame.

References:Let's stay in touch and Follow me for more thoughts and updates