<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: IderaDevTools</title>
    <description>The latest articles on DEV Community by IderaDevTools (@ideradevtools).</description>
    <link>https://dev.to/ideradevtools</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F692047%2Fa74c3570-fc25-4d45-89cb-8c37071e8a0f.jpg</url>
      <title>DEV Community: IderaDevTools</title>
      <link>https://dev.to/ideradevtools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ideradevtools"/>
    <language>en</language>
    <item>
      <title>A Receipt Reader That Draws Its OCR Back on the Photo</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:07:26 +0000</pubDate>
      <link>https://dev.to/ideradevtools/a-receipt-reader-that-draws-its-ocr-back-on-the-photo-4f2</link>
      <guid>https://dev.to/ideradevtools/a-receipt-reader-that-draws-its-ocr-back-on-the-photo-4f2</guid>
      <description>&lt;p&gt;That works until the number is wrong, and on a creased thermal receipt photographed on a restaurant table it sometimes is. Now a finance reviewer has a figure they doubt and no way to check it short of opening the original and hunting for the total by eye. The extraction saved nobody any time.&lt;/p&gt;

&lt;p&gt;Building the version that fixes this yourself is a small computer vision project: find the receipt inside the photo, warp it flat, correct the exposure, run an OCR engine, keep the word positions, and host all of it somewhere that can take a phone upload. That is a model, a worker and a queue before you have approved a single expense.&lt;/p&gt;

&lt;p&gt;This guide walks through Marlow, an expense capture app, and the detail worth stealing from it: Filestack’s &lt;code&gt;ocr&lt;/code&gt; returns a bounding box for every word, so the app can draw the extracted figure back onto the photo it came from.&lt;/p&gt;

&lt;h1&gt;
  
  
  What we’re building
&lt;/h1&gt;

&lt;p&gt;An employee photographs a receipt at any angle, in any light. The receipt is found and flattened, the colour is corrected, the text is read, and every word is outlined on the image where it was found. The claim lands in a finance queue with the total already pulled out.&lt;/p&gt;

&lt;p&gt;Checking a total becomes a glance rather than a search.&lt;/p&gt;

&lt;h1&gt;
  
  
  Stack
&lt;/h1&gt;

&lt;p&gt;Layer: Tech&lt;br&gt;&lt;br&gt;
Framework: Next.js 16 (App Router)&lt;br&gt;&lt;br&gt;
Upload, storage, CDN, processing: Filestack&lt;br&gt;&lt;br&gt;
Client state: Zustand&lt;br&gt;&lt;br&gt;
Styling: Tailwind v4&lt;br&gt;&lt;br&gt;
Language: TypeScript&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: Two keys, and why
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_FILESTACK_API_KEY=your_api_key_here
FILESTACK_APP_SECRET=your_app_secret_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The first is public by necessity, because uploads go from the phone straight to Filestack with no server hop. The second must never carry a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;The second one exists because Filestack tasks come in two kinds. &lt;code&gt;resize&lt;/code&gt;, &lt;code&gt;enhance&lt;/code&gt; and friends answer a plain URL. &lt;code&gt;ocr&lt;/code&gt; and &lt;code&gt;doc_detection&lt;/code&gt; reject unsigned requests: they need a policy signed with your app secret, which means a server has to be involved. That split shapes the whole app, and it is also where the most interesting bug in this codebase lives.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: Upload from the phone
&lt;/h1&gt;

&lt;p&gt;The upload is the shared picker helper used across this series, opened with &lt;code&gt;accept: ['image/*']&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/services/filestack.service.ts
export async function pickFiles(options: Partial&amp;lt;PickerOptions&amp;gt; = {}): Promise&amp;lt;IStoredFile[]&amp;gt; {
  const client = await getFilestackClient();
return new Promise&amp;lt;IStoredFile[]&amp;gt;((resolve, reject) =&amp;gt; {
    client
      .picker({
        fromSources: ['local_file_system', 'url', 'googledrive', 'dropbox', 'onedrive'],
        ...options,
        onUploadDone: (response) =&amp;gt; resolve((response.filesUploaded ?? []).map(formatPickerMetadata)),
        onCancel: () =&amp;gt; resolve([]),
        onFileUploadFailed: (_file, error) =&amp;gt; reject(error),
      })
      .open()
      .catch(reject);
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Wrapping the picker in a promise turns a callback API into one &lt;code&gt;await&lt;/code&gt;, which is the shape every caller in the app actually wants. A cancel resolves with an empty list rather than rejecting, so closing the picker is not treated as an error.&lt;/p&gt;

&lt;p&gt;What comes back is trimmed to one shape, and the &lt;code&gt;handle&lt;/code&gt; is the part that matters:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export interface IStoredFile {
  url: string;
  handle: string;
  filename: string;
  mimetype: string;
  size: number;
  uploadedAt?: string;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Step 3: Signing on the server
&lt;/h1&gt;

&lt;p&gt;One route holds the secret and hands back finished URLs:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/app/api/filestack/sign/route.ts
const ALLOWED = /^(ocr|envelope_ocr|doc_detection(=[a-z_]+:(true|false)(,[a-z_]+:(true|false))*)?)$/;
const TTL_SECONDS = 60 * 10;
const policy = JSON.stringify({
  expiry: Math.floor(Date.now() / 1000) + TTL_SECONDS,
  call: ['read', 'convert'],
  handle,
});
const encodedPolicy = Buffer.from(policy).toString('base64');
const signature = createHmac('sha256', secret).update(encodedPolicy).digest('hex');
const url = [CDN, `security=p:${encodedPolicy},s:${signature}`, ...tasks, handle].join('/');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three decisions here are worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The policy names one handle. A leaked URL cannot be replayed against any other file.&lt;/li&gt;
&lt;li&gt;The TTL is ten minutes. The URL is handed to a browser, so it should stop working soon after the browser is done with it.&lt;/li&gt;
&lt;li&gt;The route has a task allowlist. Without it, you have built a signing oracle: anyone can post any task chain and get it signed with your secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser side is a single fetch:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const signed = await requestSignedUrl(uploaded.handle, [
  signedTaskSegment({ task: 'doc_detection', coords: false, preprocess: true }),
  signedTaskSegment({ task: 'ocr' }),
]);
const result = (await fetch(signed).then((r) =&amp;gt; r.json())) as IOcrResult;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Step 4: Clean before you read
&lt;/h1&gt;

&lt;p&gt;The reading chain is two tasks in one URL:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;security=p:…,s:…/doc_detection=coords:false,preprocess:true/ocr/&amp;lt;handle&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Tasks apply left to right. &lt;code&gt;doc_detection&lt;/code&gt; finds the receipt inside the photo and warps it flat, removing the table, the shadow and the thumb holding it down. With &lt;code&gt;coords:false&lt;/code&gt; it returns the processed image rather than the corner coordinates, and &lt;code&gt;ocr&lt;/code&gt; reads that image instead of the original.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;ocr&lt;/code&gt; on the raw photo would read a crooked, dim picture of a table with a receipt on it. The order is the accuracy.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 5: Drawing the boxes
&lt;/h1&gt;

&lt;p&gt;The OCR response nests: &lt;code&gt;document.text_areas[].lines[].words[]&lt;/code&gt;, each word carrying a bounding box, plus a flat &lt;code&gt;text&lt;/code&gt; field and a &lt;code&gt;text_area_percentage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The component normalises defensively rather than trusting one field layout:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/components/features/ReceiptReader.tsx
if (Array.isArray(raw) &amp;amp;&amp;amp; raw.length &amp;gt;= 2) {
  // A list of points: derive the extent.
  const pts = raw as { x: number; y: number }[];
  const xs = pts.map((p) =&amp;gt; p.x);
  const ys = pts.map((p) =&amp;gt; p.y);
  x = Math.min(...xs);
  y = Math.min(...ys);
  w = Math.max(...xs) - x;
  h = Math.max(...ys) - y;
} else if (typeof raw === 'object') {
  const b = raw as { x?: number; y?: number; width?: number; height?: number };
  x = b.x; y = b.y; w = b.width; h = b.height;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then it converts every box to fractions of the image, using the rendered image’s natural size, so the overlay survives any display width:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const norm = x &amp;lt;= 1 &amp;amp;&amp;amp; y &amp;lt;= 1 &amp;amp;&amp;amp; w &amp;lt;= 1 &amp;amp;&amp;amp; h &amp;lt;= 1;
boxes.push({
  text: word.text ?? '',
  x: norm ? x : x / imgW,
  y: norm ? y : y / imgH,
  w: norm ? w : w / imgW,
  h: norm ? h : h / imgH,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The boxes belong to the processed image, not the original. They come from OCR that ran after &lt;code&gt;doc_detection&lt;/code&gt;, so they have to be drawn over that chain’s output. Overlay them on the raw upload and every box lands in the wrong place, because the receipt in the raw photo is somewhere else and at a different angle.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 6: A total you can point at
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function guessTotal(text: string): string | null {
  const matches = [...text.matchAll(/(?:total|amount due|balance)\D{0,12}([\d.,]+)/gi)];
  const last = matches.at(-1);
  if (last) return last[1];
  const money = [...text.matchAll(/\d+[.,]\d{2}/g)].map((m) =&amp;gt; m[0]);
  return money.length &amp;gt; 0 ? money.sort(/* largest first */)[0] : null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Taking the last match is deliberate. Receipts print subtotal, tax and total in that order, and “Subtotal” also matches &lt;code&gt;total&lt;/code&gt;, so the one you want is the one at the bottom.&lt;/p&gt;

&lt;p&gt;This is a heuristic and should be treated as one. It fails on a receipt that prints “Total items: 3” under the total, and the fallback’s &lt;code&gt;parseFloat&lt;/code&gt; sort reads &lt;code&gt;1,234.56&lt;/code&gt; as &lt;code&gt;1.234&lt;/code&gt;. It is acceptable here for one reason: the box is drawn on the image, so a wrong guess is visibly wrong rather than silently wrong.&lt;/p&gt;

&lt;h1&gt;
  
  
  What the browser builds and what the server signs
&lt;/h1&gt;

&lt;p&gt;This is the part of the codebase most worth arguing about, because it gets the principle right and the implementation slightly wrong.&lt;/p&gt;

&lt;p&gt;The principle: anything that answers an unsigned request can be built in the browser and dropped into an &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;. Anything that needs a policy goes through the signing route. The image the boxes are drawn on, though, is built like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cleanedTasks = ['doc_detection=coords:false,preprocess:true', 'enhance=preset:fix_dark'];
const cleanedUrl = file ? cdnUrl(file.handle, cleanedTasks) : '';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is an unsigned URL containing &lt;code&gt;doc_detection&lt;/code&gt;, which the app itself says rejects unsigned requests. Either the preview fails to load and the boxes have nothing to sit on, or your account accepts it unsigned and the signing route’s comment is wrong. One of them has to change, and it is the preview. Sign it.&lt;/p&gt;

&lt;p&gt;That exposes the second problem. The two chains are not the same:&lt;/p&gt;

&lt;p&gt;ChainTasksShown on screen:&lt;code&gt;doc_detection&lt;/code&gt; → &lt;code&gt;enhance&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Read by OCR:&lt;code&gt;doc_detection&lt;/code&gt; → &lt;code&gt;ocr&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The product copy says the receipt is “flattened, colour corrected and read, in that order, because each step makes the next more accurate”. The reading chain skips the colour correction, so the step that is meant to help OCR never reaches it. The boxes still line up, because &lt;code&gt;enhance&lt;/code&gt; does not move pixels, but the reader is working from a dimmer image than the one on screen.&lt;/p&gt;

&lt;p&gt;The fix is one chain, used twice:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const chain = ['doc_detection=coords:false,preprocess:true', 'enhance=preset:fix_dark'];
const imageUrl = await requestSignedUrl(handle, chain);
const ocrUrl = await requestSignedUrl(handle, [...chain, 'ocr']);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That needs &lt;code&gt;enhance&lt;/code&gt; added to the route’s allowlist. Do that deliberately, as a fixed pattern like &lt;code&gt;enhance=preset:(fix_dark|fix_tint)&lt;/code&gt;, rather than loosening the regex.&lt;/p&gt;

&lt;h1&gt;
  
  
  One small layout bug worth knowing about
&lt;/h1&gt;

&lt;p&gt;The boxes are positioned with &lt;code&gt;left: calc(${box.x * 100}% + 0.5rem)&lt;/code&gt; inside a card with &lt;code&gt;p-2&lt;/code&gt; padding. Absolute percentages resolve against the card’s padding box, which is 1rem wider than the image. So boxes drift right by up to a rem towards the far edge of the receipt. On a narrow phone screen that is enough to put the box half on the wrong digit.&lt;/p&gt;

&lt;p&gt;Wrap the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and the boxes in their own relative element with no padding, and use plain percentages. The coordinates are already fractions of the image, so they should be positioned against the image.&lt;/p&gt;

&lt;h1&gt;
  
  
  Beyond expenses
&lt;/h1&gt;

&lt;p&gt;Surface: Filestack feature&lt;br&gt;&lt;br&gt;
Invoice and purchase order capture: doc_detection, ocr&lt;br&gt;&lt;br&gt;
ID and proof-of-address onboarding: doc_detection, ocr&lt;br&gt;&lt;br&gt;
Delivery notes and proof of delivery: ocr, with boxes for the signature block&lt;br&gt;&lt;br&gt;
Mileage logs photographed from a diary: doc_detection, enhance, ocr&lt;br&gt;&lt;br&gt;
Screening uploads from personal devices: virus_detection in a Workflow&lt;br&gt;&lt;br&gt;
Running the chain once, on upload: Workflows&lt;/p&gt;

&lt;p&gt;The boxes are useful anywhere a human has to approve what a machine read. Most integrations throw them away.&lt;/p&gt;

&lt;h1&gt;
  
  
  Production checklist
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Sign the preview chain. It contains &lt;code&gt;doc_detection&lt;/code&gt;, so build it through the signing route like the OCR chain.&lt;/li&gt;
&lt;li&gt;Read the same image you show. Put &lt;code&gt;enhance&lt;/code&gt; in the OCR chain, and add it to the allowlist as a fixed pattern.&lt;/li&gt;
&lt;li&gt;Fix the overlay offset by positioning boxes against the image, not the padded card.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;guessTotal&lt;/code&gt; with a real parser that handles thousands separators and currency, and keep the boxes either way.&lt;/li&gt;
&lt;li&gt;Move the chain into a Workflow so one upload triggers everything server side, and store the OCR result against the claim instead of re-reading it.&lt;/li&gt;
&lt;li&gt;Downscale before &lt;code&gt;doc_detection&lt;/code&gt;. It has a maximum input size, and phone photos are large.&lt;/li&gt;
&lt;li&gt;Check your plan’s quotas for &lt;code&gt;ocr&lt;/code&gt; and &lt;code&gt;doc_detection&lt;/code&gt; before launch. They are metered separately from transformations.&lt;/li&gt;
&lt;li&gt;Move submissions out of &lt;code&gt;localStorage&lt;/code&gt; and put real auth in front of the finance queue.&lt;/li&gt;
&lt;li&gt;Configure Security Policies: origin lock, &lt;code&gt;image/*&lt;/code&gt;, a size cap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Key takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Show where the number came from.&lt;/strong&gt; Filestack’s &lt;code&gt;ocr&lt;/code&gt; returns a bounding box for every word, so the extracted total can be drawn back onto the receipt. A wrong value becomes visibly wrong instead of silently wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two kinds of tasks, two kinds of URLs.&lt;/strong&gt; &lt;code&gt;resize&lt;/code&gt; and &lt;code&gt;enhance&lt;/code&gt; answer plain URLs. &lt;code&gt;ocr&lt;/code&gt; and &lt;code&gt;doc_detection&lt;/code&gt; need a signed policy, so the app secret lives on a server route and never in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign narrowly.&lt;/strong&gt; Scope each policy to one handle, expire it in ten minutes, and allowlist the tasks the route will sign. Skip the allowlist and you have built a signing oracle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order is accuracy.&lt;/strong&gt; Run &lt;code&gt;doc_detection&lt;/code&gt; before &lt;code&gt;ocr&lt;/code&gt; so the reader sees a flat, cropped receipt instead of a crooked photo of a table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw boxes on the image OCR actually read.&lt;/strong&gt; The coordinates belong to the processed output, not the raw upload, and they should be positioned against the image itself rather than a padded card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One chain, signed once, used twice.&lt;/strong&gt; Build the preview and the OCR request from the same chain (&lt;code&gt;doc_detection&lt;/code&gt;, &lt;code&gt;enhance&lt;/code&gt;, &lt;code&gt;ocr&lt;/code&gt;) so the image you show is the image you read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat heuristics as heuristics.&lt;/strong&gt; &lt;code&gt;guessTotal&lt;/code&gt; is acceptable only because its guess is visible. Replace it with a real parser that handles currency and thousands separators before going to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pattern travels.&lt;/strong&gt; Invoices, ID onboarding, proof of delivery, mileage logs: anywhere a human approves what a machine read, show them where it read it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;Extraction is not the hard part any more. Trust is.&lt;/p&gt;

&lt;p&gt;A number in a field is a claim.&lt;br&gt;&lt;br&gt;
A number with a box drawn around where it came from is evidence.&lt;/p&gt;

&lt;p&gt;Filestack gives you the second in the same response as the first. The only work left is keeping the image you show and the image you read the same, which in this app means one chain, signed once, used twice.&lt;/p&gt;

&lt;p&gt;The general shape holds well beyond receipts: whenever a machine reads something a human has to approve, show the human where it read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/show-your-working-a-receipt-reader/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>ocr</category>
    </item>
    <item>
      <title>Slow Image Editing API Responses and Where the Latency Comes From</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:52:00 +0000</pubDate>
      <link>https://dev.to/ideradevtools/slow-image-editing-api-responses-and-where-the-latency-comes-from-173n</link>
      <guid>https://dev.to/ideradevtools/slow-image-editing-api-responses-and-where-the-latency-comes-from-173n</guid>
      <description>&lt;p&gt;The dashboard says p50 is fine. Nobody’s complaining about the median. Then someone splits the data by cache status and finds p99 sitting at 4 seconds, hidden in the aggregate because cached and cold requests were averaged together as though they were the same kind of request.&lt;/p&gt;

&lt;p&gt;The real solution for slow image editing API responses is rarely a faster API. It’s identifying which of the four latency sources you’re actually paying for, because each one has a different fix. Applying the wrong fix can waste time without improving the metric that matters.&lt;/p&gt;

&lt;p&gt;Slow image editing API responses usually trace to four sources: cold transformations with no cache hit, oversized source images, chained operations executed on every request, and origin distance without a CDN. The solution stack is caching transformed outputs at the edge, right-sizing masters, and pre-warming common renditions. Filestack caches every transformation at the CDN, so repeat requests skip processing entirely.&lt;/p&gt;

&lt;p&gt;This article starts with measurement, because attribution has to come before any fix. It then walks through each of the four latency sources and the specific changes that actually address them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;First-request transformation latency and cached-request latency differ by an order of magnitude. Conflating them in one metric misdiagnoses everything downstream.&lt;/li&gt;
&lt;li&gt;A 20-megapixel master resized on every request wastes decode time permanently that a right-sized master avoids for good.&lt;/li&gt;
&lt;li&gt;Chained operations cost once, when the whole chain caches as a single output object, not once per operation on every request.&lt;/li&gt;
&lt;li&gt;Origin distance adds fixed round-trip latency that no amount of processing optimisation can recover. Only moving to the edge removes it.&lt;/li&gt;
&lt;li&gt;Pre-warming predictable renditions, generating known catalog sizes ahead of time, moves the cold cost to deploy time instead of a user’s first request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measuring, Split Cold From Cached First
&lt;/h2&gt;

&lt;p&gt;Before touching any fix, split the data. A single average latency across cold and cached requests tells you very little because it combines two fundamentally different operations. A transformation that processes an image from scratch and one that serves an already-cached result are not the same request with slightly different timings. They’re different orders of magnitude.&lt;/p&gt;

&lt;p&gt;Instrument cache status directly. Most CDN and transformation layers expose a cache-status header on the response, such as hit or miss. Log that alongside latency because it’s one of the most useful signals for understanding where the time is going.&lt;/p&gt;

&lt;p&gt;Report p50 and p99 separately, and break them down by rendition too. A thumbnail and a full-resolution hero image can have very different cold-path costs even before caching is considered.&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;Once cold and cached requests are separated, the waterfall above becomes the map for what follows: four distinct costs stack up on a cold request, while a cached request skips most of them. Each segment in the cold-request path has its own cause and its own fix, which the next three sections take in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1, Cache Misses, The Silent Majority
&lt;/h2&gt;

&lt;p&gt;The single biggest lever in many systems is &lt;strong&gt;cache stability&lt;/strong&gt;. If an apparently identical rendition generates a cache miss on every request, nothing else in the latency stack matters yet. You’re repeatedly paying the full cold-path cost for work that should have been done once.&lt;/p&gt;

&lt;p&gt;The usual culprit is an &lt;strong&gt;unstable URL&lt;/strong&gt;: query parameters appear in different orders, a per-user signature is added even though the underlying transformation is identical, or a timestamp or session token makes an otherwise cacheable URL unique. Each variation can create a separate cache key, turning one reusable rendition into repeated cache misses.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;canonical, deterministic transformation URLs&lt;/strong&gt;. Express the same transformation the same way every time and remove incidental variations that defeat caching. This connects directly to what tools provide a CDN for delivering user-uploaded files: a CDN can only reuse a cached object when requests map to the same cache key, and unnecessary URL variation prevents that reuse.&lt;/p&gt;

&lt;p&gt;Stabilising URLs can eliminate a large class of latency problems before you need to optimise the transformation itself. The remaining causes matter for requests that are genuinely cold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Causes 2 and 3, Source Size and Chain Order
&lt;/h2&gt;

&lt;p&gt;A cold request that remains slow after cache stability is fixed usually points to one of two causes inside the transformation itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source size is the first.&lt;/strong&gt; A 20-megapixel master that gets decoded and resized for every cold request can add unnecessary processing time. If your largest rendition is 2,000 pixels wide, starting from a 6,000-pixel-wide master means decoding far more pixel data than the output needs. Keeping a reasonably sized working copy alongside the original can reduce that cost at the source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgvo4b5jvoqlmha7trvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgvo4b5jvoqlmha7trvf.png" alt=" " width="720" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chain order is the second.&lt;/strong&gt; For chained operations such as resize, crop, and watermark, it generally makes sense to perform expensive size-reducing operations early so later steps work on fewer pixels. For example, resizing before applying a watermark avoids performing the overlay across a full-resolution image when most of those pixels will be discarded afterwards.&lt;/p&gt;

&lt;p&gt;This is the same principle behind what are the best practices for optimising image loading times on websites: do expensive work on the smallest practical amount of data as early in the pipeline as possible.&lt;/p&gt;

&lt;p&gt;Optimising the transformation handles the costs within your pipeline’s control. The fourth cause sits outside the transformation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4, Origin Distance
&lt;/h2&gt;

&lt;p&gt;Origin distance is different from the first three causes in one important way: processing optimisation can’t fix it. A user thousands of miles from your origin still pays the network distance whenever a request has to reach that origin, regardless of how quickly the transformation itself runs.&lt;/p&gt;

&lt;p&gt;This is fundamentally a geography problem, so the fix is geographic too: serve content from edge locations closer to users instead of relying on a single distant origin. A CDN with a distributed edge network can serve a cached transformation from an edge near the requester, reducing the distance between the user and the content even when the origin is elsewhere.&lt;/p&gt;

&lt;p&gt;This is the practical core of what’s the best API for image and video transformation on upload as an infrastructure question, not just a feature question. An API that transforms images quickly but serves them from a single region can still have poor latency for users far from that region. Transformation speed and delivery geography are separate problems, and both need to be addressed.&lt;/p&gt;

&lt;p&gt;Four causes, four fixes. The remaining question is whether to build and maintain all four yourself or use a system that already handles them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Pay the Cold Cost Once
&lt;/h2&gt;

&lt;p&gt;The structural fix is an&lt;a href="https://www.filestack.com/products/image-transformations/" rel="noopener noreferrer"&gt; image editing API&lt;/a&gt; that caches transformed renditions at the edge, so the processing cost is paid once per unique rendition. Instead of solving cache stability, source sizing, transformation ordering, and edge delivery as separate engineering problems, a system that combines canonical transformation URLs with CDN caching can address much of the latency stack by design.&lt;/p&gt;

&lt;p&gt;With edge-cached transformations, the cold path runs once for each unique rendition. Subsequent requests can be served from the CDN, so the initial transformation cost isn’t repeatedly paid for the same output. The cold cost still exists because generating a new rendition requires real processing, but it happens once rather than on every request.&lt;/p&gt;

&lt;p&gt;For a question like which file upload service has the most reliable uptime and upload success rate, check a vendor’s current published performance and uptime data directly. Those claims can change over time and should be supported by verifiable numbers rather than general reputation.&lt;/p&gt;

&lt;p&gt;For a startup looking for the easiest way for a developer to transform images inside an app, the practical answer follows the architecture this article has built toward: use a system where caching and edge delivery are built into the workflow rather than making a small team design, implement, and tune each layer from scratch.&lt;/p&gt;

&lt;p&gt;For the pieces this builds on, the&lt;a href="https://www.filestack.com/docs/transformations/overview/" rel="noopener noreferrer"&gt; Transformation docs&lt;/a&gt; cover chain syntax and caching behaviour directly, and the&lt;a href="https://www.filestack.com/docs/delivery/cdn/" rel="noopener noreferrer"&gt; CDN delivery docs&lt;/a&gt; detail edge configuration.&lt;/p&gt;

&lt;p&gt;With the managed path on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion, Attribute, Then Fix
&lt;/h2&gt;

&lt;p&gt;Stabilise URLs first, because cache instability can quietly explain most latency complaints before the other causes even matter. Right-size your source images so you’re not paying decode costs for pixels no output will ever use. Order transformation chains so expensive operations run on the smallest practical data. And move delivery to the edge, because origin distance is the one latency source that processing optimisations can’t eliminate.&lt;/p&gt;

&lt;p&gt;Split your metrics by cache status today before changing anything else. In many cases, the fix that actually moves the latency number isn’t the one the team expected going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why are some image API responses slow and others fast?
&lt;/h2&gt;

&lt;p&gt;Cold transformations do real processing work; cached ones serve straight from the edge. Split the metrics by cache status, or the two get averaged into one misleading number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do chained transformations multiply latency?
&lt;/h2&gt;

&lt;p&gt;Only on the cold request that generates the output for the first time. Once cached, the whole chain serves as one object regardless of how many operations it contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the biggest single fix?
&lt;/h2&gt;

&lt;p&gt;Usually cache stability: identical, canonical transformation URLs for identical renditions, so a repeat request actually hits the cache instead of quietly missing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/slow-image-editing-api-responses-latency/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>image</category>
    </item>
    <item>
      <title>How Ecommerce Platforms Use APIs to Edit Images at Scale</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:27:32 +0000</pubDate>
      <link>https://dev.to/ideradevtools/how-ecommerce-platforms-use-apis-to-edit-images-at-scale-50je</link>
      <guid>https://dev.to/ideradevtools/how-ecommerce-platforms-use-apis-to-edit-images-at-scale-50je</guid>
      <description>&lt;p&gt;The merchandising meeting lands a simple-sounding request: the brand refresh needs new crops and a watermark on 12,000 product images by Friday. The team editing by hand starts doing the math and quietly panics. The team with a URL template changes one line and moves on with their day.&lt;/p&gt;

&lt;p&gt;Ask how ecommerce platforms use APIs to edit images at scale, and the answer is a discipline, not a tool: keep one master image, generate renditions on request, and eliminate manual edits. At catalog scale, image editing stops being a design task and becomes configuration.&lt;/p&gt;

&lt;p&gt;Ecommerce platforms edit images at scale by moving transformation out of design tools and into APIs: one master image per product, with crops, resizes, formats, and watermarks generated on request through URL parameters and cached at the CDN. This yields consistent catalogs without manual editing. Filestack’s transformation API applies chained operations through a simple URL syntax.&lt;/p&gt;

&lt;p&gt;This article covers the math that makes hand-editing impractical at real catalog scale, how URL-based image transformation works, and what it takes to keep those generated renditions fast once they’re in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A 10,000-SKU catalog across 6 surfaces and 3 formats implies 180,000 renditions. Nobody is hand-making that many images, and nobody should try.&lt;/li&gt;
&lt;li&gt;URL-based transformation encodes the edit in the request itself, so renditions need no pre-generation or storage of their own.&lt;/li&gt;
&lt;li&gt;Chained operations: resize, crop, format, watermark, execute in one pass and cache as a single CDN object, not four separate round trips.&lt;/li&gt;
&lt;li&gt;Format negotiation to WebP or AVIF at the edge cuts catalog page weight materially, with zero re-uploads required.&lt;/li&gt;
&lt;li&gt;A one-master-image policy means a reshoot updates every surface automatically, since every rendition derives from the same source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Catalog Problem, One Master, Many Renditions
&lt;/h2&gt;

&lt;p&gt;Do the math on a real catalog before anything else. Ten thousand SKUs, six surfaces: thumbnail, product page hero, mobile card, search result, email banner, and zoom view, and three formats per surface for browser compatibility and file size. That’s 180,000 individual renditions, and it’s still a modest catalog by ecommerce standards.&lt;/p&gt;

&lt;p&gt;Hand-editing that many images isn’t a staffing problem you solve by adding more people. It’s a workflow that stops making sense when every file needs individual attention. The model that works at this scale is one master image per product, with each rendition generated on demand instead of being created and stored separately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwjiqmqhgin0zijz7rmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwjiqmqhgin0zijz7rmd.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This connects directly to what’s the best API for image and video transformation on upload: the right answer isn’t simply a tool that edits faster. It’s a system that eliminates the need for a human to make the same edit 180,000 times in the first place.&lt;/p&gt;

&lt;p&gt;Upload the master image once, then express each rendition as a reusable URL transformation. That replaces the manual editing workflow structurally rather than simply making it faster.&lt;/p&gt;

&lt;p&gt;Understanding the scale of the problem explains why URL-based transformation exists. The next question is how it actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL-Based Transformation: The Edit in the Request
&lt;/h2&gt;

&lt;p&gt;A URL-based transformation encodes the edit directly in the request URL: resize dimensions, crop coordinates, output format, watermark overlays, and more. The master image stays untouched. The transformation happens when the image is requested, and the resulting rendition can be cached at the CDN like any other static asset.&lt;/p&gt;

&lt;p&gt;Here’s a product hero image resized and converted to WebP:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;https://cdn.filestackapi.com/AbCdEf123/resize=width:1200,height:1200/format=webp/SOURCE_HANDLE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Everything after the source handle represents the transformation: resize to 1200 × 1200, then convert to WebP. Change the dimensions, and you get a different rendition. Request the same URL again, and the cached result can be reused.&lt;/p&gt;

&lt;p&gt;Here’s the same master image cropped for a mobile card and given a seasonal campaign watermark:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;https://cdn.filestackapi.com/AbCdEf123/crop=dim:[100,100,800,800]/watermark=file:BADGE_HANDLE,position:bottom-right/resize=width:400/SOURCE_HANDLE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That’s three operations in one request: crop to a square region, add the watermark in the bottom-right, then resize the result for mobile. The operations run as one transformation pipeline, producing one requested rendition rather than requiring separate processing and stored files for each step.&lt;/p&gt;

&lt;p&gt;This directly answers what’s the best way to add image transformation on the fly for resize, crop, and watermarking: express the operations in the URL in the order they should run, then let the transformation service handle processing and caching.&lt;/p&gt;

&lt;p&gt;The URL is also easy to read and compare in code review. A reviewer can see which transformation changed without having to inspect two image files manually.&lt;/p&gt;

&lt;p&gt;The real leverage comes from generating these URLs automatically from your catalog rules rather than hand-crafting them for every product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automations, Thumbnails, Badges, Watermarks
&lt;/h2&gt;

&lt;p&gt;Most rendition generation shouldn’t require a human to construct a URL at all. Event-driven automation is what makes this scale: when a new product image is uploaded, the thumbnail, hero crop, and mobile variant can be generated automatically as part of the upload workflow, with no separate manual step.&lt;/p&gt;

&lt;p&gt;This is the practical answer to how do I generate image thumbnails automatically after upload at catalog scale: connect thumbnail generation to the upload pipeline and use a fixed transformation template. Every new product image gets its standard renditions as soon as it arrives, without anyone having to remember to request them.&lt;/p&gt;

&lt;p&gt;Campaign overlays work the same way. Apply a seasonal sale badge, “new arrival” label, or temporary watermark through URL parameters rather than modifying the master image. One shared template can apply the overlay across an entire collection. When the campaign ends, change the template once, and the overlay disappears everywhere it was applied.&lt;/p&gt;

&lt;p&gt;Automated renditions handle the routine cases well. Keeping those renditions fast to load is a separate problem, and it’s worth treating it separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance, Cache and Formats
&lt;/h2&gt;

&lt;p&gt;A transformed image is only useful if it loads quickly, and this is where caching and format choice do most of the work. Each unique transformation URL can be cached at the CDN as its own object after the first request, so subsequent requests can be served from the cache instead of repeating the transformation.&lt;/p&gt;

&lt;p&gt;Format choice matters too. Serving WebP or AVIF instead of JPEG can reduce file size while maintaining similar visual quality, particularly for browsers that support those formats. You don’t need to re-upload the master image or maintain a separate asset pipeline. The transformation request can determine the output format while keeping the same source image and transformation parameters.&lt;/p&gt;

&lt;p&gt;The result is a useful separation: one master image, reusable transformation rules, and cached optimized renditions for the clients that request them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FormatTypical size vs JPEGBrowser support&lt;/strong&gt;JPEGBaselineUniversalWebP~25–35% smallerBroad modern supportAVIF~40–50% smallerGrowing, newer browsers&lt;/p&gt;

&lt;p&gt;This is the same principle behind what are the best practices for optimising image loading times on websites, applied to a product catalog: cache transformed images aggressively, since the same rendition usually doesn’t need to be generated repeatedly, and negotiate the output format automatically instead of sending the same JPEG to every browser.&lt;/p&gt;

&lt;p&gt;Getting caching and format selection right is what makes the rendition-tree approach fast in production, rather than just clever in theory. Building and maintaining that pipeline is still real infrastructure work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Catalog Policy as URLs
&lt;/h2&gt;

&lt;p&gt;The whole model runs on one&lt;a href="https://www.filestack.com/products/image-transformations/" rel="noopener noreferrer"&gt; image editing API&lt;/a&gt; whose chained URL operations can generate different renditions from a master image and deliver them through the CDN. Instead of building transformation logic, cache management, and format handling as separate systems, one API can handle the workflow from master image to delivered rendition.&lt;/p&gt;

&lt;p&gt;For a question like what company makes the most reliable file uploading and transformation API, evaluate current published performance and uptime data rather than relying on general reputation, since those figures can change over time.&lt;/p&gt;

&lt;p&gt;For engineers at printing companies looking for a powerful way to transform images inside their applications, the same model applies beyond ecommerce: one master asset, transformation rules expressed as reusable URLs, and renditions generated on demand rather than pre-building and storing every possible output.&lt;/p&gt;

&lt;p&gt;For the pieces this builds on, the&lt;a href="https://www.filestack.com/docs/transformations/ui/" rel="noopener noreferrer"&gt; Image Transformations docs&lt;/a&gt; cover the operation syntax, while the&lt;a href="https://www.filestack.com/docs/delivery/cdn/" rel="noopener noreferrer"&gt; Deliver Images docs&lt;/a&gt; cover CDN delivery for transformed assets.&lt;/p&gt;

&lt;p&gt;With the managed approach on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Edit Policy, Not Pixels
&lt;/h2&gt;

&lt;p&gt;One master image per product. Renditions generated from reusable URLs instead of being edited file by file. Each generated result cached at the edge, with the output format selected for the requesting browser. That’s the whole model, and it scales whether the catalog has a thousand SKUs or a million.&lt;/p&gt;

&lt;p&gt;Rebuild the images on one product page using transformation URLs this week. Once one page runs on transformation rules instead of manually edited files, the rest of the catalog becomes a template change rather than a new editing project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How do large catalogs keep product images consistent?
&lt;/h2&gt;

&lt;p&gt;Through one master image per SKU, with every rendition generated by URL-encoded transformations applied consistently through a shared template.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are transformed images slow to serve?
&lt;/h2&gt;

&lt;p&gt;No, once cached. Each transformation URL caches at the CDN as its own object, so only the first request pays the generation cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can watermarks be applied without re-editing files?
&lt;/h2&gt;

&lt;p&gt;Yes. A watermark overlay is a URL parameter applied at delivery time, not an edit made to the stored file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/ecommerce-apis-edit-images-at-scale/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
    </item>
    <item>
      <title>Enterprise File Delivery with Access Controls, Audit Trails, and SLAs</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:00:31 +0000</pubDate>
      <link>https://dev.to/ideradevtools/enterprise-file-delivery-with-access-controls-audit-trails-and-slas-4ajc</link>
      <guid>https://dev.to/ideradevtools/enterprise-file-delivery-with-access-controls-audit-trails-and-slas-4ajc</guid>
      <description>&lt;p&gt;The deal is paused on one question in the security review: “Can you show us who downloaded this file?” Not “Can you deliver it quickly?” Not “Does the link work?” Just that one question, and the answer can determine whether the quarter closes on time.&lt;/p&gt;

&lt;p&gt;Enterprise file delivery is judged twice: once by users measuring seconds and once by auditors measuring answers. Speed matters, and CDNs and Range requests are what make large downloads fast and resilient. But what separates enterprise delivery is everything that sits on top of that speed: controlled access, auditability, and guarantees around availability.&lt;/p&gt;

&lt;p&gt;Enterprise file delivery adds governance to speed: signed URLs and access policies decide who can fetch a file, audit trails record every access, and SLAs plus multi-region CDN delivery guarantee availability. Filestack combines CDN delivery with policy and signature-based security, expiring URLs, and compliance controls suited to regulated teams.&lt;/p&gt;

&lt;p&gt;This article covers each of those three additions directly: what signed URLs and access policies actually control, what an audit trail needs to answer during a security review, and what an SLA can realistically promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Signed, expiring URLs scope each download to a specific bearer and time window, replacing the shared static link that anyone with the URL can use forever.&lt;/li&gt;
&lt;li&gt;Access policies express permissions, read access, expiry, and IP constraints, verifiable at the edge before any bytes move.&lt;/li&gt;
&lt;li&gt;An audit trail that answers who fetched what and when is the difference between resolving an incident and having no answer at all.&lt;/li&gt;
&lt;li&gt;SOC 2 compliance and GDPR-aligned processing are the certification questions procurement asks first, before anything about features.&lt;/li&gt;
&lt;li&gt;Multi-region CDN delivery is what backs an availability promise an SLA can actually stand behind, rather than a number picked because it sounds reassuring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Access Control, Signed URLs and Policies
&lt;/h2&gt;

&lt;p&gt;A shared static link is the default failure mode of casual file delivery: one URL, no expiry, and access for anyone who gets hold of it, including someone it was never intended for.&lt;/p&gt;

&lt;p&gt;Enterprise delivery replaces that with signed, expiring URLs, where each link is scoped to a specific access window and stops working when that window closes.&lt;/p&gt;

&lt;p&gt;Access policies go deeper than the URL itself. They define the permissions attached to a download, such as read-only access, an explicit expiry time, or, in some systems, an IP range restriction. The important architectural detail is that these rules should be enforced before the file is delivered, not discovered afterwards when an audit log shows that an unauthorised download already happened.&lt;/p&gt;

&lt;p&gt;This is also where procurement questions about what security certifications file upload API providers offer, such as SOC 2 or GDPR-related compliance, come in. Check a vendor’s current compliance documentation directly because certifications and their scope can change.&lt;/p&gt;

&lt;p&gt;Access control determines who can fetch a file. What happens after that decision, and what gets recorded each time, is where the next layer of governance begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit Trails, The Unanswerable Question, Answered
&lt;/h2&gt;

&lt;p&gt;The paused-deal question from the introduction, “Can you show us who downloaded this file?”, has one acceptable answer: “Yes. Here’s the log.” Anything weaker, “we don’t track it,” “we could probably reconstruct it,” or “let us check”, can turn a routine security review into a stalled deal.&lt;/p&gt;

&lt;p&gt;A real audit trail should answer four questions for every access: who accessed the file, what file they accessed, when the access happened, and where the request came from. That’s the minimum bar. Anything less is closer to server logs that someone hopes will be complete enough when an auditor asks, rather than a system designed to answer the question on demand.&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;Retention and export matter as much as capture. A log that exists but can’t be exported in the format a compliance workflow or external auditor requires is only half useful. Retention also needs to match the applicable regulatory requirements for the data being distributed, which can vary by industry and by the contents of the files. Confirm those requirements for your specific use case rather than assuming a vendor’s default retention period is sufficient.&lt;/p&gt;

&lt;p&gt;An audit trail turns “we think” into “here’s the record.” The next governance layer, SLAs, turns “it’s usually up” into a number someone has actually agreed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  SLAs and Multi-Region Availability
&lt;/h2&gt;

&lt;p&gt;An SLA is only as valuable as what the underlying infrastructure can honestly support. A number chosen because it sounds reassuring, without the architecture to back it up, can create a false sense of security that an actual incident quickly exposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-region CDN delivery&lt;/strong&gt; is what makes a high-availability promise credible. If files are served from only one region, a regional outage can take the entire delivery path down. Distributing delivery across multiple regions means a failure in one region doesn’t necessarily take the whole service with it. That’s the engineering reality an SLA should reflect.&lt;/p&gt;

&lt;p&gt;This connects directly to which file upload service has the most reliable uptime and upload success rate, viewed from the delivery side. Check a vendor’s current published uptime figures and status history rather than relying only on the SLA percentage in a contract. The contractual guarantee and actual service history answer different questions.&lt;/p&gt;

&lt;p&gt;Getting access controls, audit trails, and a genuinely supportable SLA right requires real infrastructure to build and maintain. It’s worth understanding that cost before deciding whether to build it yourself or use a managed service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Build-vs-Buy Ledger at Enterprise Scale
&lt;/h2&gt;

&lt;p&gt;Building this in-house means owning signed URL generation and rotation, an access policy engine, audit log capture and export, and multi-region CDN infrastructure as ongoing responsibilities, not a one-time project. Each piece needs maintenance as compliance requirements evolve, new regions are added, and customers introduce new audit or procurement requirements.&lt;/p&gt;

&lt;p&gt;That’s the real substance behind what’s the ROI of using a managed file upload API vs. building in-house at enterprise scale. The comparison isn’t just the initial build cost. A capable team might build a first version of much of this in a quarter, but the ongoing cost is keeping four separate governance systems secure, current, and audit-ready indefinitely.&lt;/p&gt;

&lt;p&gt;The right comparison is the cost of maintaining that infrastructure internally versus the cost of using a managed system that already carries those responsibilities. The answer won’t be the same for every organisation, and it shouldn’t be assumed either way. Run the numbers against your actual engineering, compliance, infrastructure, and operational costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Governance Under One Roof
&lt;/h2&gt;

&lt;p&gt;The packaged version is governed&lt;a href="https://www.filestack.com/products/deliver-files/" rel="noopener noreferrer"&gt; file delivery&lt;/a&gt; with signed URLs, policy enforcement, and CDN delivery under one roof. Instead of building and maintaining access control, audit logging, and multi-region delivery as separate systems, the governance layers come together as part of the delivery workflow.&lt;/p&gt;

&lt;p&gt;This is close to what tools provide a CDN for delivering user-uploaded files taken to its enterprise conclusion: the CDN speed layer and the governance layers on top of it, rather than separate integrations that each need to be maintained. Filestack security policies and signatures can apply to both uploads and downloads, giving teams a more consistent permission model across the file lifecycle.&lt;/p&gt;

&lt;p&gt;For questions like what’s the best file upload API for enterprise-scale applications, or the leadership question of for an edtech company, what’s the most comprehensive solution for a CTO to deliver files anywhere, the architectural answer is similar across industries: combine delivery performance and governance rather than bolting a separately managed access-control layer onto a CDN.&lt;/p&gt;

&lt;p&gt;For the pieces this builds on, the&lt;a href="https://www.filestack.com/docs/security" rel="noopener noreferrer"&gt; Security and compliance docs&lt;/a&gt; cover certification and policy details, while the&lt;a href="https://www.filestack.com/docs/delivery" rel="noopener noreferrer"&gt; Deliver Files product docs&lt;/a&gt; cover the delivery configuration.&lt;/p&gt;

&lt;p&gt;With the managed approach on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Fast Is Table Stakes, Provable Wins Deals
&lt;/h2&gt;

&lt;p&gt;Sign every URL so no download link works forever for anyone who happens to have it. Log every access so the question that pauses a deal has an immediate, complete answer instead of a scramble. Promise only the availability your architecture can actually support, because an SLA nobody can verify during an incident isn’t much of a guarantee.&lt;/p&gt;

&lt;p&gt;Run the procurement checklist below against your current delivery path today, before an auditor runs it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What distinguishes enterprise file delivery?
&lt;/h2&gt;

&lt;p&gt;Access controls, per-access audit trails, and SLA-backed availability, layered on top of the same CDN speed every delivery system needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How are enterprise downloads secured?
&lt;/h2&gt;

&lt;p&gt;Through signed, expiring URLs scoped by an access policy and verified at the edge before any bytes move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What certifications matter for delivery vendors?
&lt;/h2&gt;

&lt;p&gt;SOC 2 and GDPR alignment are the baseline procurement asks. Worth confirming a vendor’s current certification status directly rather than assuming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/enterprise-file-delivery-access-audit-slas/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>file</category>
      <category>delivery</category>
    </item>
    <item>
      <title>Large File Delivery with Caching, Range Requests, and Resumable Downloads</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:14:43 +0000</pubDate>
      <link>https://dev.to/ideradevtools/large-file-delivery-with-caching-range-requests-and-resumable-downloads-31c0</link>
      <guid>https://dev.to/ideradevtools/large-file-delivery-with-caching-range-requests-and-resumable-downloads-31c0</guid>
      <description>&lt;p&gt;A 4GB dataset download reaches 91% on hotel Wi-Fi. The connection drops for two seconds. The server says, “start over.” The user says, “support ticket,” and probably with a lot less patience the second time.&lt;/p&gt;

&lt;p&gt;Large file delivery fails quietly. A download that stalls at 91% and starts from zero is the result of a design choice, or the lack of one. Uploads have had resumability for years, with chunked transfers and progress that survives dropped connections treated as standard features. Downloads deserve the same three techniques, but many teams never built them in.&lt;/p&gt;

&lt;p&gt;Large file delivery is the download side of the upload problem: serving multi-gigabyte files fast and resumably. The core techniques are CDN edge caching, HTTP Range requests for partial and resumable downloads, and cache-friendly immutable URLs. Filestack delivers stored files through an integrated CDN with range support, so large assets stream instead of stall.&lt;/p&gt;

&lt;p&gt;This article covers why large downloads fail by default, how Range requests provide resumability, how caching improves delivery speed, and what changes when the file is video.&lt;/p&gt;

&lt;h1&gt;
  
  
  Key Takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;HTTP Range requests, backed by &lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; and &lt;code&gt;206 Partial Content&lt;/code&gt; responses, enable pause, resume, and parallel segment downloads.&lt;/li&gt;
&lt;li&gt;Edge caching moves multi-gigabyte transfers to servers physically near the user, cutting both latency and origin egress cost at once.&lt;/li&gt;
&lt;li&gt;Immutable, versioned URLs allow &lt;code&gt;Cache-Control: immutable&lt;/code&gt; with a long &lt;code&gt;max-age&lt;/code&gt;, the single highest-leverage caching setting for static files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ETag&lt;/code&gt; combined with &lt;code&gt;If-Range&lt;/code&gt; prevents the classic corrupted-resume bug: resuming a download partway through, against a file that changed underneath it.&lt;/li&gt;
&lt;li&gt;Chunk-parallel download of ranged segments can saturate a client’s available bandwidth in a way a single stream often can’t.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Why Big Downloads Fail
&lt;/h1&gt;

&lt;p&gt;Three separate problems compound into the stalled download from the intro, and none of them is exotic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin distance&lt;/strong&gt; is the first. A file served from a single origin server, no matter how fast that server is, still has to cross the physical and network distance between it and the user. Someone on another continent pays that distance for every byte.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-stream fragility&lt;/strong&gt; is the second. A download running as one continuous HTTP request has no recovery path if the connection drops. At 91% of a 4GB file, one failed connection can send the whole transfer back to zero because the server was never asked to resume from where it stopped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache misses&lt;/strong&gt; complete the problem. Without caching, every download hits the origin, even when thousands of users request the same file. That’s slower for users and can increase origin egress costs.&lt;/p&gt;

&lt;p&gt;This is closely related to what tools provide a CDN for delivering user-uploaded files. Solving all three problems usually means the same infrastructure: a CDN reduces the impact of origin distance, Range requests provide resumability, and caching prevents repeated downloads from hitting the origin unnecessarily.&lt;/p&gt;

&lt;p&gt;Each of these three problems has its own fix, and they’re worth taking one at a time, starting with the one that turns a stalled download into a resumable one.&lt;/p&gt;

&lt;h1&gt;
  
  
  Range Requests and Resume
&lt;/h1&gt;

&lt;p&gt;A server that supports Range requests advertises that support with the &lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; header. A client can then request only part of a file using the &lt;code&gt;Range&lt;/code&gt; header, and a server that honors the request responds with &lt;code&gt;206 Partial Content&lt;/code&gt; and sends only the requested bytes.&lt;/p&gt;

&lt;p&gt;This turns “start over” into “pick up where you left off.” A client that knows how many bytes it already has can resume a failed download from that offset instead of throwing away its progress.&lt;/p&gt;

&lt;p&gt;There is one important problem with naive resume logic: the file might have changed between attempts. If the client combines the original bytes with bytes from a newer version, the resulting file can be corrupted without being immediately obvious.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;If-Range&lt;/code&gt; prevent that. The client saves the &lt;code&gt;ETag&lt;/code&gt; from the original response and sends it back in &lt;code&gt;If-Range&lt;/code&gt; when resuming. If the &lt;code&gt;ETag&lt;/code&gt; still matches, the server can safely return the requested range. If it has changed, the server sends the complete file instead, preventing two different versions from being stitched together.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function resumableDownload(url, existingBytes = 0, etag = null) {
const headers = {};
if (existingBytes &amp;gt; 0) {
headers["Range"] = `bytes=${existingBytes}-`;
if (etag) headers["If-Range"] = etag;
}
const response = await fetch(url, { headers });
if (response.status === 206) {
// Partial content: append to what we already have
const newEtag = response.headers.get("ETag");
const chunk = await response.arrayBuffer();
return { chunk, etag: newEtag, resumed: true };
}
if (response.status === 200) {
// Full file: either a fresh download, or the file changed since we last tried
const newEtag = response.headers.get("ETag");
const full = await response.arrayBuffer();
return { chunk: full, etag: newEtag, resumed: false };
}
throw new Error(`Unexpected status: ${response.status}`);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A client built this way handles both cases without special-case logic: a genuine resume gets a &lt;code&gt;206 Partial Content&lt;/code&gt; response and appends the missing bytes, while a changed file gets a fresh &lt;code&gt;200 OK&lt;/code&gt; response and starts clean. The same resume flow handles both safely.&lt;/p&gt;

&lt;p&gt;Range requests solve the fragility problem. They don’t, by themselves, solve the distance or redundant-fetch problems from the previous section. That’s where caching comes in.&lt;/p&gt;

&lt;h1&gt;
  
  
  Caching Strategy, Immutable URLs and TTLs
&lt;/h1&gt;

&lt;p&gt;The highest-leverage caching decision for a large static file is also the simplest: if the file’s content never changes at a given URL, tell caches they can keep it with &lt;code&gt;Cache-Control: immutable&lt;/code&gt; and a long &lt;code&gt;max-age&lt;/code&gt;. A cache can then serve repeat requests without revalidating against the origin, including requests from many different users.&lt;/p&gt;

&lt;p&gt;The trick that makes this safe is putting a version identifier in the URL instead of relying on cache invalidation when content changes. For example, &lt;code&gt;/assets/report-v3.pdf&lt;/code&gt; can be cached for a long time because the next version uses a different URL, such as &lt;code&gt;/assets/report-v4.pdf&lt;/code&gt;, rather than replacing the existing resource.&lt;/p&gt;

&lt;p&gt;Purging still has a place, but it should be the exception rather than the normal update mechanism. Use it when content genuinely needs to disappear, such as after a takedown or security issue. For routine updates, versioning the URL is more predictable and doesn’t depend on every cache in the path processing a purge request quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HeaderPurposeTypical value for large static files&lt;/strong&gt;&lt;code&gt;Accept-Ranges&lt;/code&gt;Advertises Range request support&lt;code&gt;bytesContent-Range&lt;/code&gt;States which byte range a 206 response covers&lt;code&gt;bytes 91000000-4294967295/4294967296ETag&lt;/code&gt;Identifies a specific file version for validationA hash or version string unique to that file’s content&lt;code&gt;If-Range&lt;/code&gt;Makes a Range request conditional on ETag matchThe ETag value from a previous response&lt;code&gt;Cache-Control&lt;/code&gt;Sets caching behaviour for the response&lt;code&gt;public, max-age=31536000, immutable&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This same discipline is the broader answer to what are the best practices for optimising image loading times on websites, applied to large static assets generally: version the URL, cache aggressively with immutable resources, and let the CDN serve repeat requests without repeatedly fetching the same bytes from the origin.&lt;/p&gt;

&lt;p&gt;Caching and Range requests solve delivery for most large files. Video adds another wrinkle worth handling separately.&lt;/p&gt;

&lt;h1&gt;
  
  
  Video and Media Specifics
&lt;/h1&gt;

&lt;p&gt;Progressive playback, where a video starts playing before the entire file has downloaded, relies on the same Range request mechanism covered above. The player requests the byte ranges it needs as playback continues, and a server or CDN that supports ranges can deliver those portions without requiring the entire file upfront.&lt;/p&gt;

&lt;p&gt;This works well for a single quality level, but it has a limit. It doesn’t support &lt;strong&gt;adaptive bitrate switching&lt;/strong&gt;, where the player moves to a lower quality when the network slows down or a higher quality when conditions improve.&lt;/p&gt;

&lt;p&gt;That’s where HLS or DASH becomes the better approach. The video is transcoded into multiple quality levels and divided into short segments, allowing the player to switch between them as network conditions change.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: Range-based progressive download is fine for a single-quality video or a large non-video asset. Once adaptive quality matters, move to segmented streaming instead of stretching Range requests beyond what they’re designed to do.&lt;/p&gt;

&lt;p&gt;Whichever approach you use, the underlying question remains the same: is delivery actually reliable in practice? That’s something worth measuring rather than assuming.&lt;/p&gt;

&lt;h1&gt;
  
  
  Measuring Delivery Reliability
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Completion rate&lt;/strong&gt; is the most direct signal: what percentage of started downloads actually finish. If completion drops on mobile networks or in certain regions, it’s often a sign of the distance or connection-fragility problems covered earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resume rate&lt;/strong&gt; shows how often stalled downloads successfully recover instead of being abandoned. A high stall rate combined with a low resume rate suggests the Range and resume flow isn’t working as intended. A reliable resume path should turn most stalls into recoveries rather than failed downloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;p95 time-to-first-byte&lt;/strong&gt; is especially useful for large file delivery because the average can hide the users having the worst experience. Tracking the 95th percentile shows how the slower end of the distribution is performing instead of letting fast requests mask the bad ones.&lt;/p&gt;

&lt;p&gt;These metrics are also useful when evaluating which file upload service has the most reliable uptime and upload success rate, with the same caveat on the download side: check a provider’s current, verifiable numbers rather than relying on general reputation.&lt;/p&gt;

&lt;p&gt;Measuring reliability tells you whether the techniques above are actually working in production. Building and maintaining all three correctly is still a significant infrastructure commitment.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Managed Route, Delivery Attached to Storage
&lt;/h1&gt;

&lt;p&gt;The integrated version is&lt;a href="https://www.filestack.com/products/deliver-files/" rel="noopener noreferrer"&gt; file delivery&lt;/a&gt; through a CDN that supports Range requests, connected to the same pipeline the files entered through. Files uploaded through the pipeline can be delivered through the CDN without requiring a separate delivery system.&lt;/p&gt;

&lt;p&gt;That’s the delivery half of a symmetry many teams overlook: upload reliability gets careful engineering attention, while downloads are often served using whatever the framework provides by default. For a product team asking what’s the most responsive way to deliver files anywhere, such as high-resolution property photos and floor plans that need to load quickly for buyers around the world, the answer is architectural before it’s vendor-specific: combine edge caching with Range support and connect that delivery layer to your file storage from the start.&lt;/p&gt;

&lt;p&gt;For the pieces this builds on, the&lt;a href="https://www.filestack.com/docs/delivery/cdn/" rel="noopener noreferrer"&gt; Deliver Files product docs&lt;/a&gt; cover the CDN configuration directly, the&lt;a href="https://blog.filestack.com/file-delivery-101/" rel="noopener noreferrer"&gt; File Delivery 101 guide&lt;/a&gt; covers CDN delivery at scale, and the&lt;a href="https://blog.filestack.com/mobile-file-upload-large-files-slow-connections/" rel="noopener noreferrer"&gt; large-file mobile upload guide&lt;/a&gt; covers the other half of this same symmetry, getting large files in reliably in the first place.&lt;/p&gt;

&lt;p&gt;With the managed approach on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion: Never Make Anyone Start Over
&lt;/h1&gt;

&lt;p&gt;Cache at the edge so repeat requests don’t unnecessarily reach the origin. Support Range requests so a dropped connection can resume instead of starting over. Version your URLs so you can cache aggressively without serving stale content. Use ETags to make sure a resumed download never combines bytes from two different versions of the same file.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;curl -r 0-1023&lt;/code&gt; against your own delivery URL today and check the response. If you get a &lt;code&gt;200 OK&lt;/code&gt; with the entire file instead of a &lt;code&gt;206 Partial Content&lt;/code&gt; response containing the requested range, you’ve found exactly the kind of delivery gap this article describes.&lt;/p&gt;

&lt;h1&gt;
  
  
  Frequently Asked Questions
&lt;/h1&gt;

&lt;h1&gt;
  
  
  What makes a download resumable?
&lt;/h1&gt;

&lt;p&gt;Server support for Range requests, returning &lt;code&gt;206 Partial Content&lt;/code&gt; for a byte range, validated against file changes using &lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;If-Range&lt;/code&gt; so a resume never corrupts against an updated file.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is the best cache setting for large static files?
&lt;/h1&gt;

&lt;p&gt;A versioned URL with &lt;code&gt;Cache-Control: immutable&lt;/code&gt; and a long &lt;code&gt;max-age&lt;/code&gt;. Putting the version in the URL means the cache never needs to revalidate or guess whether content changed.&lt;/p&gt;

&lt;h1&gt;
  
  
  Does Filestack support ranged delivery?
&lt;/h1&gt;

&lt;p&gt;Yes. Files stored through Filestack serve through a CDN with range support built in, so large assets stream and resume rather than stalling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Originally published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/large-file-delivery-caching-range-requests/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>file</category>
      <category>delivery</category>
    </item>
    <item>
      <title>Cross-Platform PDF Data Extraction SDK for Multiple Clients</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Thu, 08 Oct 2026 23:17:19 +0000</pubDate>
      <link>https://dev.to/ideradevtools/cross-platform-pdf-data-extraction-sdk-for-multiple-clients-531e</link>
      <guid>https://dev.to/ideradevtools/cross-platform-pdf-data-extraction-sdk-for-multiple-clients-531e</guid>
      <description>&lt;p&gt;The same invoice PDF produces a different total on Android than it does on the web. Support can’t reproduce the issue because both teams are using different OCR engines, and both implementations can be working correctly. The engines have simply changed at different rates over time.&lt;/p&gt;

&lt;p&gt;The goal of a cross-platform PDF data extraction SDK isn’t just to reuse code. It’s to make sure the same invoice produces the same result no matter which client uploads it.&lt;/p&gt;

&lt;p&gt;That consistency doesn’t happen automatically. It’s an architecture decision you need to make from the start, not something to patch after the results start drifting.&lt;/p&gt;

&lt;p&gt;A cross-platform PDF data extraction SDK exposes one extraction pipeline to many clients: web, iOS, Android, and server. The architecture that scales keeps OCR and parsing server-side behind a single API while thin per-platform SDKs handle capture and upload. Filestack Capture follows this model, with JavaScript and mobile SDKs feeding one processing pipeline and one result schema.&lt;/p&gt;

&lt;p&gt;This article makes the case for that architecture directly. It explains what belongs on the client versus the server and how to keep extraction results consistent as the pipeline evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Duplicating OCR per platform multiplies model drift: three engines end up with three accuracy profiles and three separate bug queues.&lt;/li&gt;
&lt;li&gt;Thin-client architecture puts capture and upload in the SDK, and OCR, layout detection, and schema mapping behind one versioned API.&lt;/li&gt;
&lt;li&gt;A single result schema with per-field confidence keeps downstream validation code platform-agnostic, since it never needs to know which client produced a given field.&lt;/li&gt;
&lt;li&gt;Server-side extraction lets heavy recognition models run on real hardware, not whatever CPU happens to be in a five-year-old phone.&lt;/li&gt;
&lt;li&gt;Idempotent document IDs allow re-extraction after a pipeline upgrade without any change on the client side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Pipeline, Many Clients: The Argument
&lt;/h2&gt;

&lt;p&gt;The alternative to one shared pipeline is a separate recognition engine for every platform: one OCR library inside the iOS app, another inside Android, and perhaps a third running server-side for web uploads. Each gets updated on its own schedule, tuned by a different team, and tested against a different set of documents.&lt;/p&gt;

&lt;p&gt;That’s the divergence problem from the introduction, built into the architecture rather than happening by accident. Over time, three engines develop three different accuracy profiles. A fix or improvement made to one doesn’t reach the others unless someone remembers to implement it separately.&lt;/p&gt;

&lt;p&gt;Centralising extraction behind one API removes that failure mode. There’s one accuracy profile to monitor, one place to improve when a document type causes problems, and one bug queue instead of three.&lt;/p&gt;

&lt;p&gt;The cost argument follows the same logic. Maintaining and tuning one recognition pipeline is generally simpler than maintaining three, especially when you include the engineering time spent tracking down bugs that reproduce on only one platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfy53b0lnzzhs1zkr3ij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfy53b0lnzzhs1zkr3ij.png" alt=" " width="720" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the architecture worth defending, not just adopting by default. The honest counterargument is on-device recognition, which has real advantages: it works offline and avoids a network round trip.&lt;/p&gt;

&lt;p&gt;But for anything beyond lightweight pre-checks, those benefits don’t outweigh what a centralised pipeline gives you in consistency, maintainability, and a single place to improve the extraction system.&lt;/p&gt;

&lt;p&gt;The next section draws that line more precisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thin Client, Fat Server in Practice
&lt;/h2&gt;

&lt;p&gt;The dividing line is simple: the SDK captures, uploads, and retries. The API extracts, detects layout, and returns a schema. OCR, layout detection, and field parsing shouldn’t live inside individual platform SDKs, even if adding a small local recognition feature seems convenient at first.&lt;/p&gt;

&lt;p&gt;Capture is platform-specific by necessity. A camera API on iOS is very different from a file input on the web. Upload and retry logic can be more consistent, but each platform still needs an SDK that fits its networking patterns.&lt;/p&gt;

&lt;p&gt;Once the file reaches the server, extraction should be completely platform-agnostic. It shouldn’t matter whether the upload came from a browser, phone, or another backend service.&lt;/p&gt;

&lt;p&gt;On the server, this is also the practical shape of how to integrate a file upload API with Node.js: a Node service accepts the upload, starts extraction, and returns a document ID that the client can poll or use with a webhook. The server-side flow stays the same regardless of where the file originated.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Node.js: minimal server-side extraction endpoint
const express = require("express");
const app = express();
app.post("/documents", async (req, res) =&amp;gt; {
  const { fileUrl } = req.body;
  const document = await extractionPipeline.submit({
    source: fileUrl,
    // No platform field needed. The pipeline treats every source identically.
  });
  res.json({ documentId: document.id, status: "processing" });
});
app.get("/documents/:id", async (req, res) =&amp;gt; {
  const result = await extractionPipeline.getResult(req.params.id);
  res.json(result); // same schema regardless of which client uploaded
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Keeping this boundary clean is an architecture decision, not just a code organisation preference. Once a “quick optimisation” adds recognition logic to one client, that client has quietly become its own extraction pipeline, and the consistency guarantee starts to disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Results Consistent
&lt;/h2&gt;

&lt;p&gt;A single pipeline solves the divergence problem when it ships. Keeping it consistent as the pipeline evolves takes more discipline.&lt;/p&gt;

&lt;p&gt;Version the pipeline explicitly. When you change the OCR model or a layout detection rule, treat it as a new pipeline version rather than silently changing the existing one. That lets you pin clients or documents to a known version and trace regressions back to a specific change.&lt;/p&gt;

&lt;p&gt;Maintain a golden-document test set. Keep a fixed collection of representative invoices, forms, contracts, and scanned documents. Run them through every new pipeline version before release and compare the output field by field. This is the organisational answer to what’s the best way to add OCR or text extraction to uploaded PDFs: extraction quality needs to be tested with every change, not checked once and forgotten.&lt;/p&gt;

&lt;p&gt;Calibrate confidence scores across sources. A 0.95 confidence score should mean roughly the same level of reliability whether the document came from a phone camera or a desktop scanner. If one source produces more errors at the same confidence level, recalibrate the scores using real outcomes so downstream systems can continue to rely on them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6evkcc081dhv70dr73qa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6evkcc081dhv70dr73qa.png" alt=" " width="720" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With consistency handled on the server, the client SDK can stay simpler rather than becoming more complex. That makes its responsibilities easier to define and is the next part of the architecture to design deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDK Surface Design
&lt;/h2&gt;

&lt;p&gt;A thin client SDK should expose a genuinely small surface: capture, upload, and a document ID that the client can poll or subscribe to. The extraction internals shouldn’t leak into the client API. If a developer needs to know which OCR model ran or how layout detection made a decision, the SDK surface has become too broad.&lt;/p&gt;

&lt;p&gt;This minimal design also makes platform support easier. The question of what SDKs are available for adding file uploads to iOS and Android apps matters because a thin SDK is relatively small and maintainable across platforms. An SDK that includes its own recognition pipeline is much harder to keep consistent across iOS, Android, web, and server, which brings back the divergence problem this architecture is meant to prevent.&lt;/p&gt;

&lt;p&gt;Idempotent document IDs complete the API boundary. If a document is submitted twice, whether because of a client retry or a deliberate re-extraction after a pipeline upgrade, the system should recognise it and avoid creating duplicate records. This also lets you reprocess existing documents with a new pipeline version without changing the client, because the client only deals with the document ID, not the extraction logic behind it.&lt;/p&gt;

&lt;p&gt;Keeping the SDK this narrow is a deliberate constraint. It’s also what makes the cross-platform architecture maintainable in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Consistency as a Service
&lt;/h2&gt;

&lt;p&gt;This architecture is available as a managed&lt;a href="https://www.filestack.com/products/filestack-capture/" rel="noopener noreferrer"&gt; data extraction SDK&lt;/a&gt; with platform-specific capture SDKs feeding into a centralised extraction pipeline and consistent output. Instead of building the thin client SDKs, pipeline versioning, and golden-document testing infrastructure yourself, you can use an integration that follows this architecture.&lt;/p&gt;

&lt;p&gt;This also addresses what API can convert and optimise documents after upload while keeping processing centralised. Format conversion and optimisation can happen alongside extraction, so documents from different clients follow the same processing path.&lt;/p&gt;

&lt;p&gt;For developers at startups looking for an easier way to build complex file-processing pipelines, the practical lesson is simple: avoid creating separate recognition engines for each platform unless you have a strong reason to do so. Keeping recognition centralised prevents the platform-specific drift that can lead to inconsistent results later.&lt;/p&gt;

&lt;p&gt;Filestack Capture provides web and mobile capture capabilities through a centralised processing workflow, so improvements to the processing pipeline can be applied consistently rather than implemented separately for each platform.&lt;/p&gt;

&lt;p&gt;For the underlying pieces, the&lt;a href="https://www.filestack.com/docs/api" rel="noopener noreferrer"&gt; Capture API docs&lt;/a&gt; cover the extraction APIs, the&lt;a href="https://blog.filestack.com/pdf-data-extraction-sdk-structured-fields" rel="noopener noreferrer"&gt; PDF extraction companion post&lt;/a&gt; covers the pipeline stages, and the&lt;a href="https://blog.filestack.com/implementing-efficient-mobile-ocr-filestack-developers-guide" rel="noopener noreferrer"&gt; mobile extraction post&lt;/a&gt; covers the mobile capture side.&lt;/p&gt;

&lt;p&gt;With the managed approach on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Centralise the Brain, Distribute the Eyes
&lt;/h2&gt;

&lt;p&gt;One pipeline, one schema, thin SDKs on every platform, and versioned upgrades that don’t require a client release. That’s the architecture: a decision made early, rather than a fix after three platforms have already drifted apart.&lt;/p&gt;

&lt;p&gt;Send the same test PDF from two different platforms through your pipeline today and compare the resulting JSON. If anything differs beyond fields such as the document ID or timestamp, you’ve found the kind of cross-platform inconsistency this architecture is designed to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Should OCR run on device or server for cross-platform apps?
&lt;/h2&gt;

&lt;p&gt;Server-side, behind one API, for consistency across every client. On-device processing has a place for lightweight pre-checks, like rejecting a blurry photo before upload, but not for the recognition itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is result consistency maintained across platforms?
&lt;/h2&gt;

&lt;p&gt;Through one versioned pipeline, one result schema, and a golden-document regression test set that runs against every pipeline change before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the client SDK contain?
&lt;/h2&gt;

&lt;p&gt;Capture, upload, and retry logic only. Extraction, layout detection, and schema mapping stay centralised on the server, regardless of platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/cross-platform-pdf-data-extraction-sdk/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>pdf</category>
      <category>sdk</category>
    </item>
    <item>
      <title>PDF Data Extraction SDK for Scanned Pages and Structured Fields</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:28:18 +0000</pubDate>
      <link>https://dev.to/ideradevtools/pdf-data-extraction-sdk-for-scanned-pages-and-structured-fields-3gda</link>
      <guid>https://dev.to/ideradevtools/pdf-data-extraction-sdk-for-scanned-pages-and-structured-fields-3gda</guid>
      <description>&lt;p&gt;An operations team retypes 400 scanned invoices into a spreadsheet every month. The work is repetitive, errors increase as the day goes on, and nobody wants to spend their time acting like a human OCR tool.&lt;/p&gt;

&lt;p&gt;A PDF data extraction SDK is useful for exactly these kinds of documents: scanned, skewed, or messy pages that are difficult to process by hand.&lt;/p&gt;

&lt;p&gt;Data extraction isn’t just one model doing all the work. It’s a pipeline, and the OCR model is only one of the five stages.&lt;/p&gt;

&lt;p&gt;A PDF data extraction SDK converts static or scanned PDFs into structured fields: it applies OCR to page images, detects layout elements such as tables, key-value pairs, and checkboxes, and returns machine-readable JSON. Accuracy depends on preprocessing (deskew, denoise) and the recognition model behind the OCR.&lt;a href="https://www.filestack.com/products/filestack-capture/" rel="noopener noreferrer"&gt; Filestack Capture&lt;/a&gt; pairs upload with OCR-driven extraction, turning documents into data in one pipeline.&lt;/p&gt;

&lt;p&gt;This article explains how PDF extraction actually works, where scanned and digital PDFs differ, how data is pulled from a flat page, and how to build a pipeline that knows when to trust the results and when to send them to a human for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Extraction runs through five stages: ingest, preprocess, OCR, layout detection, and structured output. The OCR model is one stage, not the whole pipeline.&lt;/li&gt;
&lt;li&gt;Born-digital PDFs carry a text layer that can be read directly. Scanned PDFs are images, and only images need OCR.&lt;/li&gt;
&lt;li&gt;300 DPI is the practical floor for reliable OCR. Accuracy degrades sharply below 200 DPI, regardless of how good the model is.&lt;/li&gt;
&lt;li&gt;Layout detection is what separates real extraction from plain text extraction. Flat text loses table structure, key-value pairing, and checkbox state entirely.&lt;/li&gt;
&lt;li&gt;Every extracted field should carry a confidence score, so low-confidence fields route to a human reviewer instead of silently entering your system wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From Pixels to Fields: How Extraction Works
&lt;/h2&gt;

&lt;p&gt;Extraction is a pipeline, not a single function call. It’s useful to understand what happens at each stage before you trust the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingest&lt;/strong&gt; brings the PDF into the pipeline, whether it comes from an upload, email attachment, or document scanner. &lt;strong&gt;Preprocessing&lt;/strong&gt; cleans up the page before anything reads it. This can include straightening a crooked scan, removing noise, and improving contrast so faint text is easier to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OCR&lt;/strong&gt; turns the page image into text and identifies where that text appears. &lt;strong&gt;Layout detection&lt;/strong&gt; goes a step further by figuring out which text belongs to a table, which text forms a label and value, and which marks are checkboxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output&lt;/strong&gt; then puts the extracted information into a format such as JSON with named fields, so another system can use it.&lt;/p&gt;

&lt;p&gt;This five-stage approach gives a better answer to how to add OCR or text extraction to uploaded PDFs than simply saying “run OCR.” OCR gives you raw text. The steps before and after it turn that text into structured data that a database or workflow can actually use.&lt;/p&gt;

&lt;p&gt;Each stage can fail on its own. Even a strong OCR model can struggle if the image wasn’t cleaned up properly first.&lt;/p&gt;

&lt;p&gt;Before we look at how structure is extracted, it’s important to separate the two main types of PDFs. They need very different approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scanned vs Born-Digital PDFs
&lt;/h2&gt;

&lt;p&gt;Not every PDF needs OCR, and running OCR when it isn’t needed just adds extra work. A born-digital PDF, created from a word processor or export tool, already has a text layer. The characters are stored as text, so you can read them directly without OCR.&lt;/p&gt;

&lt;p&gt;A scanned PDF is different. It’s basically an image of a page inside a PDF file. Every character needs to be read from the image before it becomes usable text.&lt;/p&gt;

&lt;p&gt;Checking for a text layer first is quick and can save processing time. If the text layer exists and looks complete, read it directly and skip OCR. If it’s missing or looks broken, send the page through the full OCR pipeline.&lt;/p&gt;

&lt;p&gt;For PDFs that do need OCR, scan quality has a major effect on accuracy. Around 300 DPI is a good target for reliable OCR. Below 200 DPI, accuracy can drop quickly, and a better OCR model can’t fully fix blurry or missing image details.&lt;/p&gt;

&lt;p&gt;Crooked pages and heavy compression can make things worse too. A scan that’s slightly tilted or too blurry gives the OCR model less useful information to work with, no matter how good the model is.&lt;/p&gt;

&lt;p&gt;Preprocessing, such as deskewing, removing noise, and improving contrast, can often improve OCR accuracy more than switching to a different OCR model. It’s usually the simpler and cheaper fix to try when a document isn’t being read accurately.&lt;/p&gt;

&lt;p&gt;Getting clean text from a PDF is only half the job. You also need to understand which pieces of text belong together. That’s where plain text extraction starts to fall short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure Detection, Tables and Key-Value Pairs
&lt;/h2&gt;

&lt;p&gt;Plain text extraction turns a page into a stream of characters, but an invoice is more than that. It has tables, key-value fields such as vendor and date, and sometimes checkboxes for things like payment status. With plain extraction, that structure can get lost. Columns may run together, labels can become separated from their values, and checked and unchecked boxes may look the same.&lt;/p&gt;

&lt;p&gt;Layout detection keeps this structure intact. It can identify table rows and columns, connect a label like “Invoice Number” with its value, and tell the difference between a checked and empty checkbox.&lt;/p&gt;

&lt;p&gt;This also connects to a broader question: what API can convert and optimise documents after upload? Extraction is often just one step. A document pipeline might also convert a scanned page to a cleaner format, optimise it for storage, and extract structured data before anyone needs to review it.&lt;/p&gt;

&lt;p&gt;The output from a well-built extraction pipeline might look like this for a simple invoice:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
"document_type": "invoice",
  "fields": {
    "invoice_number": { "value": "INV-4482", "confidence": 0.98 },
    "invoice_date": { "value": "2026-07-14", "confidence": 0.95 },
    "vendor_name": { "value": "Acme Supply Co.", "confidence": 0.91 },
    "total_due": { "value": "1,240.50", "confidence": 0.97 }
  },
  "line_items": [
    { "description": "Widget A", "quantity": 12, "unit_price": 45.00, "confidence": 0.93 },
    { "description": "Widget B", "quantity": 5, "unit_price": 112.10, "confidence": 0.68 }
  ],
  "checkboxes": {
    "paid": { "checked": false, "confidence": 0.89 }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Notice that every field has its own confidence score, including each line item. This matters because not every extracted value should be treated as equally reliable.&lt;/p&gt;

&lt;p&gt;A good extraction pipeline can use these scores to send uncertain fields to a human for review. For example, “Widget B” with 0.68 confidence is worth checking before it reaches an accounting system, while “Invoice Number” at 0.98 is much more likely to be correct.&lt;/p&gt;

&lt;p&gt;Confidence-scored, structured output is a useful starting point, but it’s not the end of the pipeline. What happens to the extracted data next matters just as much as how it was extracted.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pipeline Design: Validate What the Model Guesses
&lt;/h1&gt;

&lt;p&gt;A model’s confidence score is a starting point for validation, not a replacement for it. A field marked as 0.95 confident can still be wrong in a way that looks reasonable, which is exactly the kind of mistake schema validation can catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence-based review queues&lt;/strong&gt; offer a practical middle ground between full automation and manual data entry. Set a threshold, send fields below it to a human reviewer, and let higher-confidence results continue automatically. The right threshold depends on the business. Set it too low and reviewers see too much. Set it too high and incorrect data may pass through unchecked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema validation&lt;/strong&gt; catches a different type of problem. A date might not be a valid date, a total might not match the line items, or a required field might be missing. These checks work separately from the model’s confidence score and can catch problems the model can’t identify itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent reprocessing&lt;/strong&gt; is important too. Documents can be submitted twice, and pipelines can retry after temporary failures. Without idempotency, the same invoice could create duplicate records. Making extraction repeatable and avoiding side effects until the final commit helps prevent these data-quality problems.&lt;/p&gt;

&lt;p&gt;This gets to the bigger question of what’s the most comprehensive tool for building complex file-processing pipelines. The answer isn’t only about choosing the most accurate model. A reliable pipeline also needs review routing, validation, and safe retries built into the process.&lt;/p&gt;

&lt;p&gt;Good pipeline design is what makes extraction reliable at scale. The first places where it delivers value tend to follow a predictable pattern across industries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry Beat: Where This Pays First
&lt;/h2&gt;

&lt;p&gt;Invoices are an obvious starting point and often the easiest win. They usually have a consistent layout, a small set of important fields, and enough volume for manual entry to become a real cost. Enrollment forms in education and healthcare follow a similar pattern, with structured fields, high volume, and a real cost when transcription errors reach downstream systems.&lt;/p&gt;

&lt;p&gt;Print businesses handling customer-uploaded files are another strong use case. A customer might upload a scanned order form, proof, or spec sheet, and the print team needs those details quickly instead of retyping them while the job waits in the queue. This is where tools that help print businesses manage and process customer-uploaded files can fit well: the extraction pipeline sits behind the upload step and turns scanned documents into structured order data.&lt;/p&gt;

&lt;p&gt;The common thread is high volume, predictable layouts, and a real cost to manual processing, whether that’s staff time or data-entry errors. Extraction delivers the most value when all three are present. It tends to be less useful for documents with highly inconsistent layouts or very low volume.&lt;/p&gt;

&lt;p&gt;Building the full pipeline, from preprocessing through review routing, is a real engineering project. There’s also a managed approach where much of that work is already built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route: Upload to JSON in One Call
&lt;/h2&gt;

&lt;p&gt;The assembled version of this pipeline is a managed&lt;a href="https://www.filestack.com/products/filestack-capture/" rel="noopener noreferrer"&gt; data extraction SDK&lt;/a&gt; that can handle the document intake, OCR, and structured extraction through one API. Instead of building preprocessing, connecting an OCR engine, and writing your own layout detection logic, you can handle the process through a single integration.&lt;/p&gt;

&lt;p&gt;Filestack Capture combines document capture, OCR, and extraction, returning structured data that can be used by your application. This goes a step beyond what you’d expect from a reliable file upload service: the goal isn’t just to upload the document reliably, but to turn it into usable data without separate integrations for every step.&lt;/p&gt;

&lt;p&gt;For more on the pieces behind this workflow, the&lt;a href="https://www.filestack.com/docs/" rel="noopener noreferrer"&gt; Filestack Capture docs&lt;/a&gt; cover the extraction API, while the&lt;a href="https://www.filestack.com/docs/transformations/intelligence/ocr/" rel="noopener noreferrer"&gt; OCR transformation docs&lt;/a&gt; explain the available recognition options. The&lt;a href="https://blog.filestack.com/implementing-efficient-mobile-ocr-filestack-developers-guide/" rel="noopener noreferrer"&gt; mobile extraction companion post&lt;/a&gt; covers capturing documents with a phone camera before they enter the extraction pipeline.&lt;/p&gt;

&lt;p&gt;With the managed approach on the table too, here’s the short version to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Trust Confidence Scores, Not Vibes
&lt;/h2&gt;

&lt;p&gt;Extraction quality depends on the whole pipeline, not just the OCR model. Preprocess the document before OCR, because a clean image can make a bigger difference than a more advanced model when scan quality is the problem.&lt;/p&gt;

&lt;p&gt;Detect the document’s structure, not just its text. Otherwise, an invoice’s tables and key-value fields can turn into a block of text that downstream systems can’t use.&lt;/p&gt;

&lt;p&gt;Finally, use confidence scores to decide what needs review instead of assuming certain documents will always work well.&lt;/p&gt;

&lt;p&gt;Test the pipeline with a real scanned document, not just a clean sample. Run it through Capture and check the confidence scores it returns. That’s a more useful test of the pipeline than relying only on an accuracy number from a spec sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What does a PDF data extraction SDK return?
&lt;/h2&gt;

&lt;p&gt;Structured JSON fields with per-field confidence scores, not just a block of raw text. The confidence scores are what let a downstream system decide what to trust automatically and what to route for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What scan quality does OCR need?
&lt;/h2&gt;

&lt;p&gt;300 DPI is the practical floor for reliable accuracy. Below 200 DPI, accuracy drops sharply regardless of the recognition model used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can tables be extracted from scanned PDFs?
&lt;/h2&gt;

&lt;p&gt;Yes, when layout detection runs alongside OCR rather than plain text extraction alone. Filestack Capture handles both in the same pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/pdf-data-extraction-sdk-structured-fields/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>sdk</category>
      <category>dataextraction</category>
    </item>
    <item>
      <title>How to Handle the AWS API Gateway 10MB Upload Limit</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Wed, 23 Sep 2026 12:55:07 +0000</pubDate>
      <link>https://dev.to/ideradevtools/how-to-handle-the-aws-api-gateway-10mb-upload-limit-1144</link>
      <guid>https://dev.to/ideradevtools/how-to-handle-the-aws-api-gateway-10mb-upload-limit-1144</guid>
      <description>&lt;p&gt;The support ticket says, &lt;strong&gt;“Uploads over 10MB fail.”&lt;/strong&gt; There’s no stack trace and no error from the app. The gateway rejected the request before it ever reached your code, and changing a setting in your account won’t change that limit.&lt;/p&gt;

&lt;p&gt;Teams building serverless apps on AWS eventually run into the AWS API Gateway file upload size limit, often through a ticket that says exactly this. The &lt;strong&gt;10 MB limit is fixed&lt;/strong&gt;, so there’s no application setting you can change to increase it.&lt;/p&gt;

&lt;p&gt;AWS API Gateway enforces a hard 10MB payload limit on REST and HTTP APIs, and no configuration raises it. Uploads bigger than that must bypass the gateway using presigned S3 URLs, S3 multipart upload, or a managed upload service. The gateway then handles only authorization and metadata, which is also the cheaper and faster architecture.&lt;/p&gt;

&lt;p&gt;This article covers why the limit exists, the two workarounds teams try first and why they fall short, and the presigned URL pattern that actually solves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway’s 10MB payload cap applies to both REST and HTTP APIs, and there is no setting that raises it.&lt;/li&gt;
&lt;li&gt;Lambda’s synchronous invocation payload cap is 6MB, and binary bodies routed through the gateway ride as base64, which inflates size by roughly 1.33x.&lt;/li&gt;
&lt;li&gt;Combined, the practical ceiling for gateway-routed binary uploads lands closer to 4–7MB of real file, well under the 10MB headline number.&lt;/li&gt;
&lt;li&gt;Presigned S3 URLs let a client upload directly to S3 using time-boxed credentials your API issues, without the file passing through the gateway at all.&lt;/li&gt;
&lt;li&gt;S3 multipart upload carries objects up to 5TB, split into parts uploaded in parallel, each with its own retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Limit and Why It Exists
&lt;/h2&gt;

&lt;p&gt;The limit is simple: &lt;strong&gt;API Gateway has a 10 MB payload limit for both REST APIs and HTTP APIs.&lt;/strong&gt; It’s not a default that you can increase later. It’s a hard limit, so changing your configuration or opening a support ticket won’t raise it.&lt;/p&gt;

&lt;p&gt;The reason makes more sense when you look at what a gateway is designed to do. API Gateway handles things like routing, authentication, and sending requests to your backend. It wasn’t built to move large files.&lt;/p&gt;

&lt;p&gt;S3, on the other hand, is built for storing and transferring large objects. It supports objects up to &lt;strong&gt;5 TB&lt;/strong&gt; and is designed for large-scale data transfers.&lt;/p&gt;

&lt;p&gt;Instead of trying to push large files through the 10 MB gateway limit, it’s better to route the file around the gateway and use each service for what it does best.&lt;/p&gt;

&lt;p&gt;That becomes even clearer when you look at what happens when teams try to work around the limit without changing the upload path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-Patterns, Base64 Bloat and Lambda Buffers
&lt;/h2&gt;

&lt;p&gt;Two common approaches show up in questions about an API for uploading files on AWS, and both look like they should work but fall short.&lt;/p&gt;

&lt;p&gt;The first is sending binary uploads through Lambda behind an API Gateway. Lambda has its own synchronous payload limit of &lt;strong&gt;6 MB&lt;/strong&gt;, which is already lower than API Gateway’s 10 MB limit. Binary request bodies can also be base64-encoded as they move through API Gateway, increasing their size by about 33%. So a 5 MB file can become roughly 6.65 MB before Lambda receives it, which can push it past Lambda’s limit.&lt;/p&gt;

&lt;p&gt;When you combine these limits, the practical upload size can be much lower than the 10 MB API Gateway limit, depending on how the request is handled.&lt;/p&gt;

&lt;p&gt;The second approach is increasing every timeout and buffer setting you can find and hoping one of them is the problem. It won’t help. The &lt;strong&gt;10 MB API Gateway limit is a hard platform limit&lt;/strong&gt;, not a setting you can increase with Lambda memory, timeouts, or gateway configuration.&lt;/p&gt;

&lt;p&gt;Both approaches make the same mistake: trying to send more data through a component that wasn’t designed to handle large file transfers.&lt;/p&gt;

&lt;p&gt;The solution isn’t a bigger pipe through the API Gateway. It’s a different upload path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Presigned URL Pattern
&lt;/h2&gt;

&lt;p&gt;A presigned URL takes a different approach instead of trying to work around the gateway limit. The client first asks your API for permission to upload. Your API then creates a short-lived, signed URL that points directly to S3. The client uploads the file using that URL.&lt;/p&gt;

&lt;p&gt;The file itself never passes through the API Gateway or Lambda.&lt;/p&gt;

&lt;p&gt;This is also the practical answer to how to set up a file upload API with your own S3 bucket at scale: let Lambda generate the presigned URL, but don’t make Lambda receive the file. The actual file upload goes straight to S3.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import boto3
import json
s3 = boto3.client("s3")
BUCKET = "your-upload-bucket"
def lambda_handler(event, context):
    body = json.loads(event["body"])
    key = body["filename"]
    presigned_url = s3.generate_presigned_url(
        ClientMethod="put_object",
        Params={"Bucket": BUCKET, "Key": key, "ContentType": body.get("contentType", "application/octet-stream")},
        ExpiresIn=300,  # 5 minutes
    )
    return {
        "statusCode": 200,
        "body": json.dumps({"uploadUrl": presigned_url, "key": key}),
    }

// Client side: request the URL, then PUT the file straight to S3

async function uploadFile(file) {
  const res = await fetch("/api/upload-url", {
    method: "POST",
    body: JSON.stringify({ filename: file.name, contentType: file.type }),
  });
  const { uploadUrl } = await res.json();
  await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The gateway now only handles a small JSON request and response. The file itself, no matter how large it is, goes directly to S3 through a connection designed to handle large uploads.&lt;/p&gt;

&lt;p&gt;A single PUT request works well for many files. For larger files, splitting the upload into smaller parts makes the process more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multipart for the Truly Large
&lt;/h2&gt;

&lt;p&gt;S3 multipart upload splits a large file into smaller parts, uploads each part separately, and puts them back together once all the parts arrive. It supports objects up to 5 TB, and parts can be uploaded in parallel and retried individually if something goes wrong.&lt;/p&gt;

&lt;p&gt;This is the practical answer to what’s the best way to handle multipart or resumable uploads for large files: don’t retry the entire file when one part fails. Retry only the part that failed.&lt;/p&gt;

&lt;p&gt;The same approach helps answer what API supports uploading files up to 5 GB reliably on unstable networks. The smaller retry units make large uploads much more reliable. If a 2 GB upload fails at 90% during one large &lt;code&gt;PUT&lt;/code&gt;, you may have to start again. With the same file split into 5 MB parts, you only need to retry the part that was interrupted.&lt;/p&gt;

&lt;p&gt;Presigned URLs and multipart uploads solve the file transfer problem well. The trade-off is that building and maintaining this infrastructure yourself takes ongoing work, even after the initial setup is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, The Pattern Productized
&lt;/h2&gt;

&lt;p&gt;The managed version of this approach is a&lt;a href="https://www.filestack.com/" rel="noopener noreferrer"&gt; file upload API&lt;/a&gt; that handles the upload infrastructure for you. It can issue upload credentials, support chunked uploads for files up to &lt;strong&gt;5 GB&lt;/strong&gt;, and keep your gateway focused on handling JSON requests.&lt;/p&gt;

&lt;p&gt;Instead of building and maintaining the presigning Lambda, client-side retry logic, and multipart upload handling yourself, you can use one integration that follows the same direct-upload approach.&lt;/p&gt;

&lt;p&gt;Filestack provides direct uploads with chunked transfers, progress events, and an embeddable picker. This is useful if you want reliable file uploads without building the entire upload system from scratch.&lt;/p&gt;

&lt;p&gt;For more on authentication and content types, see the&lt;a href="https://blog.filestack.com/file-upload-api-guide/" rel="noopener noreferrer"&gt; file upload API guide&lt;/a&gt;. You can also check the&lt;a href="https://blog.filestack.com/how-to-upload-large-files/" rel="noopener noreferrer"&gt; large file upload guide&lt;/a&gt; for handling larger files and the&lt;a href="https://www.filestack.com/docs/uploads/storage/#amazon-s3" rel="noopener noreferrer"&gt; Filestack S3 integration docs&lt;/a&gt; for the direct-to-S3 setup specifically.&lt;/p&gt;

&lt;p&gt;Now that we’ve covered both approaches, here’s a quick summary of the key points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Let the Gateway Serve JSON
&lt;/h2&gt;

&lt;p&gt;The 10 MB limit isn’t going to change, so trying to work around it with bigger buffers or longer timeouts only wastes time.&lt;/p&gt;

&lt;p&gt;The better fix is architectural: create a presigned URL, let the client upload directly to S3, and keep API Gateway focused on what it’s good at, such as authentication, routing, and small JSON requests.&lt;/p&gt;

&lt;p&gt;It’s also a simpler and more efficient path because the file doesn’t have to pass through your gateway and backend first.&lt;/p&gt;

&lt;p&gt;Try the presigned upload flow, or a managed alternative, this sprint. Either way, you’re moving the file data away from a component that wasn’t designed to carry large files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Can the API Gateway 10MB limit be increased?
&lt;/h2&gt;

&lt;p&gt;No. It’s a hard limit on both REST and HTTP APIs, and no configuration or support request raises it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do uploads under 10MB still fail through Lambda?
&lt;/h2&gt;

&lt;p&gt;Base64 encoding inflates binary bodies by roughly 1.33x on the way through the gateway, and Lambda’s synchronous payload cap sits at 6MB, tighter than the gateway’s own limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the right pattern for large uploads on AWS?
&lt;/h2&gt;

&lt;p&gt;Presigned direct-to-S3 uploads, or a managed upload service built on the same pattern. Either way, the gateway should only handle metadata, not file bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt; &lt;a href="https://blog.filestack.com/aws-api-gateway-file-upload-size-limit/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Upload File to API with Auth, Content Types and Size Limits</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:34:50 +0000</pubDate>
      <link>https://dev.to/ideradevtools/how-to-upload-file-to-api-with-auth-content-types-and-size-limits-27on</link>
      <guid>https://dev.to/ideradevtools/how-to-upload-file-to-api-with-auth-content-types-and-size-limits-27on</guid>
      <description>&lt;p&gt;The server limit was increased from 10 MB to 100 MB, but the upload still failed with a 413 error. The problem was nginx, which was still limiting request bodies to 1 MB before they reached the app.&lt;/p&gt;

&lt;p&gt;This is one of the first surprises you run into when you put file upload APIs into production: several layers can limit an upload, and changing the limit in one place doesn’t automatically change the others.&lt;/p&gt;

&lt;p&gt;To upload a file to an API, you choose a transport (&lt;code&gt;multipart/form-data&lt;/code&gt;&amp;nbsp;for forms, raw binary PUT for single files, or chunked multipart for large payloads), attach auth (bearer tokens or signed URLs), set the correct&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;, and respect size limits at every hop: client, proxy, gateway, and server. If you use Filestack, its upload API supports files up to&amp;nbsp;&lt;strong&gt;5 GB&lt;/strong&gt;&amp;nbsp;through an authenticated request.&lt;/p&gt;

&lt;p&gt;This article walks through each part of that negotiation: which transport fits which shape of upload, how auth actually gets checked, and where a request can die before it reaches your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;multipart/form-data&lt;/code&gt;&amp;nbsp;needs a boundary parameter. Setting&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;by hand without one is the most common self-inflicted 400.&lt;/li&gt;
&lt;li&gt;Raw binary PUT, using the file’s own&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;, is simpler and faster for a single file with no other form fields.&lt;/li&gt;
&lt;li&gt;Size limits stack across the request path. Client timeout, proxy body cap, gateway cap, and server parser limit all apply, and the smallest one decides the outcome.&lt;/li&gt;
&lt;li&gt;A 413 means some hop rejected the size. A 415 means it rejected the type. They get diagnosed at different layers.&lt;/li&gt;
&lt;li&gt;Signed URLs move auth into the URL itself, with an expiry, which lets a client upload straight to storage without ever holding an API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Three Transports
&lt;/h2&gt;

&lt;p&gt;Most file upload API questions start with the same decision: how should the file be sent? In most cases, you have three options.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;multipart/form-data&lt;/code&gt;&amp;nbsp;works well for forms that include a file and other fields, such as a title or description. The request is split into separate parts, and each part has its own headers.&lt;/p&gt;

&lt;p&gt;Raw binary PUT is simpler. The file is sent directly as the request body, with&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;set to the file’s actual type. It’s a good choice when you’re sending one file without any extra form fields.&lt;/p&gt;

&lt;p&gt;Chunked uploads are better for large files. The file is split into smaller parts, and each part is uploaded separately. The server then puts the parts back together after all of them arrive. This is more reliable for large uploads because a dropped connection doesn’t have to restart the entire file.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests
FILE_PATH = "photo.jpg"
API_URL = "&amp;lt;https://api.example.com/upload&amp;gt;"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}
# 1. multipart/form-data, for a file plus other fields
with open(FILE_PATH, "rb") as f:
requests.post(API_URL, headers=HEADERS,
files={"file": f}, data={"title": "Vacation photo"})
# 2. raw binary PUT, for a single file with no other fields
with open(FILE_PATH, "rb") as f:
requests.put(f"{API_URL}/photo.jpg", headers={**HEADERS, "Content-Type": "image/jpeg"},
data=f)
# 3. chunked multipart, for large files sent in parts
CHUNK_SIZE = 5 * 1024 * 1024
upload_id = requests.post(f"{API_URL}/multipart/start", headers=HEADERS).json()["id"]
with open(FILE_PATH, "rb") as f:
part_number = 1
while chunk := f.read(CHUNK_SIZE):
requests.put(f"{API_URL}/multipart/{upload_id}/{part_number}",
headers=HEADERS, data=chunk)
part_number += 1
requests.post(f"{API_URL}/multipart/{upload_id}/complete", headers=HEADERS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Choosing the right upload method gets the file moving. But before the upload can start, the API needs to check whether the request is allowed. That’s where authentication comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth Patterns
&lt;/h2&gt;

&lt;p&gt;Three common authentication methods cover most file upload APIs. Each one offers a different balance between simplicity and security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bearer tokens&lt;/strong&gt;&amp;nbsp;are the simplest option. The client sends a token in the&amp;nbsp;&lt;code&gt;Authorization&lt;/code&gt;&amp;nbsp;header, and the server checks it before accepting the upload. This works well when the user is already signed in and has a valid token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HMAC signatures&lt;/strong&gt;&amp;nbsp;add another layer of protection. The client signs the request with a shared secret, and the server checks the signature before accepting the file. This can help detect changes to the request while it’s being sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expiring signed URLs&lt;/strong&gt;&amp;nbsp;let the client upload directly to storage. The backend creates a URL with a signature and expiration time, then gives it to the client. The client can use that URL to upload without receiving a long-term API key. Once the URL expires, it can no longer be used.&lt;/p&gt;

&lt;p&gt;If you’re comparing file upload providers, it’s also worth checking their security and compliance information, such as&amp;nbsp;&lt;strong&gt;SOC 2&lt;/strong&gt;&amp;nbsp;and&amp;nbsp;&lt;strong&gt;GDPR&lt;/strong&gt;&amp;nbsp;support. These details can change, so check each provider’s current documentation instead of assuming they all offer the same protections.&lt;/p&gt;

&lt;p&gt;Authentication controls&amp;nbsp;&lt;strong&gt;who can upload&lt;/strong&gt;. Next,&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;helps determine&amp;nbsp;&lt;strong&gt;what they’re allowed to upload&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Types and the Boundary
&lt;/h2&gt;

&lt;p&gt;One of the most common 400 errors with multipart uploads happens when you set the&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;header yourself.&amp;nbsp;&lt;code&gt;multipart/form-data&lt;/code&gt;&amp;nbsp;needs a special&amp;nbsp;&lt;strong&gt;boundary&lt;/strong&gt;&amp;nbsp;that tells the server where each part begins and ends. The HTTP client creates this boundary automatically.&lt;/p&gt;

&lt;p&gt;If you set the header manually without the correct boundary, the server may not be able to read the request. It’s better to let your HTTP client set the&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;header for you.&lt;/p&gt;

&lt;p&gt;On the server, don’t treat the&amp;nbsp;&lt;code&gt;Content-Type&lt;/code&gt;&amp;nbsp;or file extension as proof of what the file actually is. They can be wrong or changed. A safer approach is to check the file’s actual bytes and detect its real format. If it doesn’t match what was declared, you can reject or convert the file.&lt;/p&gt;

&lt;p&gt;This is also a key part of building a reliable REST API file upload flow: treat the declared file type as a hint, check the actual content, and handle mismatches safely.&lt;/p&gt;

&lt;p&gt;Once transport, authentication, and file type are handled, the next concern is what happens between the client and your server. That’s where size limits come in.&lt;/p&gt;

&lt;h1&gt;
  
  
  Size Limits at Every Hop
&lt;/h1&gt;

&lt;p&gt;A file upload doesn’t go straight from the client to your application. It can pass through several hops, and each hop may have its own size limit.&lt;/p&gt;

&lt;p&gt;The client or browser can have its own timeout or upload limit. A reverse proxy like nginx can also limit request size. Its default&amp;nbsp;&lt;code&gt;client_max_body_size&lt;/code&gt;&amp;nbsp;is often 1 MB, which can be much smaller than what your application expects.&lt;/p&gt;

&lt;p&gt;An API gateway may add another limit, and your framework or request parser can have its own rules too.&lt;/p&gt;

&lt;p&gt;That’s why increasing the limit in your application isn’t always enough. Every hop between the client and your server needs to allow the file size you want to support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HopTypical defaultError if exceeded&lt;/strong&gt;Client/browserVaries by client, often no hard capRequest never sends, or times outReverse proxy (nginx)1MB body cap by default413 Payload Too LargeAPI gatewayProvider-specific, commonly 10MB or less413 Payload Too LargeServer/framework parserFramework-specific, often configurable413 or 400 depending on framework&lt;/p&gt;

&lt;p&gt;This is still part of the bigger file upload API problem, but now we’re looking at the infrastructure instead of the code.&lt;/p&gt;

&lt;p&gt;Increasing the limit in your app won’t help if the proxy in front of it rejects the request first. Check every hop, not just the one you control in your code.&lt;/p&gt;

&lt;p&gt;Keeping track of all four hops takes time, and the work doesn’t stop after the first setup. Any of those limits can change later.&lt;/p&gt;

&lt;p&gt;There is another option that avoids having to manage all of these limits yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, One Hop That Says Yes
&lt;/h2&gt;

&lt;p&gt;If you don’t want to manage and check every hop yourself, a managed&lt;a href="https://www.filestack.com/" rel="noopener noreferrer"&gt;&amp;nbsp;file upload API&lt;/a&gt;&amp;nbsp;can simplify things. You get one documented upload limit, one authentication setup, and support for files up to&amp;nbsp;&lt;strong&gt;5 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of dealing with several different limits that may not match, you have one clear limit to work with. This makes the upload setup easier to manage and maintain.&lt;/p&gt;

&lt;p&gt;When choosing a reliable file upload service, look for a few important things: a clear file size limit, a simple authentication setup, and built-in chunked uploads. You shouldn’t have to build and maintain all of that yourself.&lt;/p&gt;

&lt;p&gt;For a startup team, the easiest approach is often to choose a service that already supports the file sizes and upload methods you need. That way, you spend less time managing multiple infrastructure limits.&lt;/p&gt;

&lt;p&gt;For a framework-specific example, the&lt;a href="https://blog.filestack.com/integrate-filestack-fastapi-python-sdk/" rel="noopener noreferrer"&gt;&amp;nbsp;FastAPI companion post&lt;/a&gt;&amp;nbsp;covers how to handle file uploads with FastAPI. You can also check the&lt;a href="https://www.filestack.com/docs/api/file/#basic-authentication" rel="noopener noreferrer"&gt;&amp;nbsp;Filestack API auth docs&lt;/a&gt;&amp;nbsp;for more details on authentication and signed URLs.&lt;/p&gt;

&lt;p&gt;That’s the managed approach. Here’s a quick summary of the key points either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Know Your Hop
&lt;/h2&gt;

&lt;p&gt;Choosing a transport depends on what you’re uploading: a file with other form fields, a single file, or a large file that needs to be split into chunks.&lt;/p&gt;

&lt;p&gt;Authentication is about deciding where trust should live. You might use a server-side token, a signed request, or a URL that lets the client upload directly.&lt;/p&gt;

&lt;p&gt;But neither matters if one of the hops in between rejects the request before it reaches your app.&lt;/p&gt;

&lt;p&gt;Try sending a 100 MB test file through the full upload path today, from the client to the server. If it fails, find the first hop that rejects it. That’s the limit that actually controls your uploads, no matter what your app’s configuration says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h1&gt;
  
  
  Why does my upload return 413?
&lt;/h1&gt;

&lt;p&gt;Some hops’ body-size cap is smaller than the file. Check the proxy and gateway limits before assuming the problem is in your application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  multipart/form-data or raw binary?
&lt;/h2&gt;

&lt;p&gt;Multipart for forms with mixed fields. Raw binary PUT for a single file with no other data attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should the client set the multipart Content-Type?
&lt;/h2&gt;

&lt;p&gt;No. Let the HTTP client generate it, since it needs to include the boundary parameter, and a hand-set header usually leaves that out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt;&amp;nbsp;&lt;a href="https://blog.filestack.com/upload-file-to-api-auth-content-types-limits/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>fileupload</category>
    </item>
    <item>
      <title>FastAPI Upload File with Multipart Handling and Streaming to Storage</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:14:11 +0000</pubDate>
      <link>https://dev.to/ideradevtools/fastapi-upload-file-with-multipart-handling-and-streaming-to-storage-50c1</link>
      <guid>https://dev.to/ideradevtools/fastapi-upload-file-with-multipart-handling-and-streaming-to-storage-50c1</guid>
      <description>&lt;p&gt;A simple FastAPI endpoint using&amp;nbsp;&lt;code&gt;file: bytes&lt;/code&gt;&amp;nbsp;works fine for small demos. But try uploading a 2 GB video, and things can go wrong quickly. If your container only has 512 MB of memory, loading the whole file into memory can cause it to run out of space and crash.&lt;/p&gt;

&lt;p&gt;You may not even get a useful error. Sometimes, all you see is a memory-related message in the logs followed by a container restart.&lt;/p&gt;

&lt;p&gt;Most FastAPI upload file tutorials work fine until a large file hits a small container. FastAPI gives you a few ways to handle uploaded files, and each one uses memory differently.&lt;/p&gt;

&lt;p&gt;FastAPI uses&amp;nbsp;&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;to handle uploaded files without keeping the whole file in memory. Multipart data can be stored in a temporary file, which is much safer for large uploads.&lt;/p&gt;

&lt;p&gt;For large files, a good production setup is to validate the file early, stream it directly to object storage, and avoid loading the entire file into memory.&lt;/p&gt;

&lt;p&gt;Another option is to use a managed upload service like Filestack. This keeps the actual file transfer out of your API server, so your FastAPI app doesn’t have to handle the file bytes itself.&lt;/p&gt;

&lt;p&gt;This article covers each approach, starting with the simple buffered setup that can struggle under heavy uploads and ending with a setup that keeps the file transfer completely off your servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A&amp;nbsp;&lt;code&gt;bytes&lt;/code&gt;&amp;nbsp;parameter loads the entire upload into RAM.&amp;nbsp;&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;is the safer default for anything sizable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;wraps&amp;nbsp;&lt;code&gt;SpooledTemporaryFile&lt;/code&gt;: small files stay in memory, large ones spill to disk on their own.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;request.stream()&lt;/code&gt;&amp;nbsp;yields chunks with no temp file at all, which keeps memory flat while you proxy to storage.&lt;/li&gt;
&lt;li&gt;Content-Length checks and part-size limits belong before you read the body, not after.&lt;/li&gt;
&lt;li&gt;A managed file upload api can take bytes out of your service entirely, leaving your endpoints to handle auth and metadata only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  UploadFile Under the Hood
&lt;/h2&gt;

&lt;p&gt;FastAPI gives you two ways to accept a file, and they behave very differently under load.&lt;/p&gt;

&lt;p&gt;Declare the parameter as&amp;nbsp;&lt;code&gt;bytes&lt;/code&gt;, and FastAPI reads the whole upload into memory before your function even runs. This works for small files and fails without warning as soon as someone uploads something large.&amp;nbsp;&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;is the answer most FastAPI docs point to for a general file upload api, and for good reason: it wraps Starlette’s&amp;nbsp;&lt;code&gt;SpooledTemporaryFile&lt;/code&gt;, which keeps small files in memory and spills larger ones to disk automatically, past a configurable threshold. Your endpoint code stays the same either way. Only where the bytes live changes.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from fastapi import FastAPI, UploadFile, File
app = FastAPI()
.post("/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
# process contents, or better, stream it in chunks below
await file.close()
return {"filename": file.filename, "size": len(contents)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This basic form is fine for small files, and it’s already safer than a&amp;nbsp;&lt;code&gt;bytes&lt;/code&gt;&amp;nbsp;parameter since Starlette manages the spool for you. It still reads the full file into a variable at once, which is where the next section picks up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multipart Done Right
&lt;/h2&gt;

&lt;p&gt;A multipart request isn’t a single file. It’s a series of parts separated by a boundary string, and each part can hold a form field or a file, mixed in any order.&lt;/p&gt;

&lt;p&gt;Understanding how does multipart upload work in web applications helps explain why FastAPI needs a parser at all: the framework has to read the boundary, split the body into parts, and hand each one to the right parameter based on its name.&lt;/p&gt;

&lt;p&gt;FastAPI handles this parsing for you with&amp;nbsp;&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;Form&lt;/code&gt;, so most endpoints don’t need to work with the raw multipart request.&lt;/p&gt;

&lt;p&gt;If you’re asking how to add file uploads to a REST API, the basic approach is usually the same: accept the file as multipart form data, validate it early, and save it somewhere reliable before sending a response.&lt;/p&gt;

&lt;p&gt;The main difference between frameworks is how much of this work they handle for you.&lt;/p&gt;

&lt;p&gt;Set limits on file size and the number of parts before processing the upload. An unlimited number of file parts can use up memory or disk space, even when each file is small.&lt;/p&gt;

&lt;p&gt;Multipart parsing gets the file into your endpoint. The next challenge is sending it somewhere else without using too much memory. That’s what we’ll cover next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming Straight to Storage
&lt;/h2&gt;

&lt;p&gt;Reading the entire file into a variable still means your app has to hold the full file while processing it. For large uploads, that’s not ideal.&lt;/p&gt;

&lt;p&gt;A better approach is to stream the file directly to storage in small chunks. With&amp;nbsp;&lt;code&gt;request.stream()&lt;/code&gt;, you can read the request body piece by piece without creating a temporary file.&lt;/p&gt;

&lt;p&gt;If you’re uploading to your own S3 bucket, the flow is simple: read one chunk, send it as an S3 multipart upload part, then move to the next chunk. You only keep one chunk in memory at a time, so memory usage stays much more predictable even as file sizes grow.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import boto3
from fastapi import FastAPI, Request
app = FastAPI()
s3 = boto3.client("s3")
BUCKET = "your-upload-bucket"
CHUNK_SIZE = 5 * 1024 * 1024  # 5MB, S3's minimum part size
u/app.post("/upload-stream/{key}")
async def upload_stream(key: str, request: Request):
upload = s3.create_multipart_upload(Bucket=BUCKET, Key=key)
upload_id = upload["UploadId"]
parts = []
part_number = 1
buffer = b""
try:
async for chunk in request.stream():
buffer += chunk
while len(buffer) &amp;gt;= CHUNK_SIZE:
part_data, buffer = buffer[:CHUNK_SIZE], buffer[CHUNK_SIZE:]
result = s3.upload_part(
Bucket=BUCKET, Key=key, UploadId=upload_id,
PartNumber=part_number, Body=part_data,
)
parts.append({"PartNumber": part_number, "ETag": result["ETag"]})
part_number += 1
if buffer:
result = s3.upload_part(
Bucket=BUCKET, Key=key, UploadId=upload_id,
PartNumber=part_number, Body=buffer,
)
parts.append({"PartNumber": part_number, "ETag": result["ETag"]})
s3.complete_multipart_upload(
Bucket=BUCKET, Key=key, UploadId=upload_id,
MultipartUpload={"Parts": parts},
)
except Exception:
s3.abort_multipart_upload(Bucket=BUCKET, Key=key, UploadId=upload_id)
raise
return {"key": key, "parts": len(parts)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This approach adds a little more code, but it gives you an important benefit: memory usage stays close to&amp;nbsp;&lt;code&gt;CHUNK_SIZE&lt;/code&gt;, no matter how large the file is.&lt;/p&gt;

&lt;p&gt;That only works if you validate the incoming request first. So the next step is making sure those checks are in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, Validation and Errors
&lt;/h2&gt;

&lt;p&gt;Streaming protects your memory while the upload is running, but it doesn’t stop a bad request from starting. You should validate the request before reading any file data.&lt;/p&gt;

&lt;p&gt;First, check&amp;nbsp;&lt;code&gt;Content-Length&lt;/code&gt;&amp;nbsp;against your maximum file size. If the header is missing or can’t be trusted, keep checking the size as you read each chunk and stop as soon as the limit is reached.&lt;/p&gt;

&lt;p&gt;You should also check the actual file type. Don’t rely only on the file extension or the MIME type sent by the client because those can be wrong.&lt;/p&gt;

&lt;p&gt;These same limits apply to any REST API that handles file uploads, no matter which framework you use. FastAPI simply gives you the tools to check them early.&lt;/p&gt;

&lt;p&gt;Once your own upload endpoint is properly limited and validated, there’s another option: don’t handle the file upload yourself at all. That’s the next approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Off the Data Path
&lt;/h2&gt;

&lt;p&gt;The final option is to keep the actual file upload out of FastAPI completely. Instead, clients upload directly through a managed&lt;a href="https://www.filestack.com/" rel="noopener noreferrer"&gt;&amp;nbsp;file upload API&lt;/a&gt;, while your FastAPI endpoints handle things like authentication and file metadata.&lt;/p&gt;

&lt;p&gt;Your server can provide short-lived upload credentials; the client sends the file directly to storage, and your API saves the file details once the upload is complete.&lt;/p&gt;

&lt;p&gt;The main benefit is that the file data never passes through your FastAPI container, which keeps your server lighter and reduces memory and bandwidth pressure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hax6gpmiy2jh8joxc6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hax6gpmiy2jh8joxc6p.png" alt=" " width="799" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don’t want to build and maintain your own file upload infrastructure, a managed service can handle that part for you.&lt;/p&gt;

&lt;p&gt;Filestack’s REST API supports file uploads up to&amp;nbsp;&lt;strong&gt;5 GB&lt;/strong&gt;&amp;nbsp;and uses chunked uploads for large files. This lets your FastAPI service focus on things like authentication and file metadata instead of moving the actual file data.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests
FILESTACK_API_KEY = "YOUR_API_KEY"
def get_upload_url(filename: str) -&amp;gt; dict:
response = requests.post(
f"&amp;lt;https://www.filestackapi.com/api/store/S3?key={FILESTACK_API_KEY}&amp;gt;",
params={"filename": filename},

)

response.raise_for_status()

return response.json()  # contains the URL the client uploads to directly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Your endpoint only needs to provide the upload URL and save the returned file handle after the upload succeeds. The actual file transfer happens outside your service.&lt;/p&gt;

&lt;p&gt;Now that we’ve covered all four approaches, here’s a quick summary of what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Choose Your Memory Tier on Purpose
&lt;/h2&gt;

&lt;p&gt;FastAPI gives you four main ways to handle file uploads. The key difference is where the file data goes while it’s being uploaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer&lt;/strong&gt;&amp;nbsp;keeps the whole file in memory.&amp;nbsp;&lt;strong&gt;Spool&lt;/strong&gt;, which is what&amp;nbsp;&lt;code&gt;UploadFile&lt;/code&gt;&amp;nbsp;uses, keeps smaller files in memory and moves larger ones to disk.&amp;nbsp;&lt;strong&gt;Stream&lt;/strong&gt;&amp;nbsp;sends the file in chunks directly to storage, keeping memory usage low.&amp;nbsp;&lt;strong&gt;Bypass&lt;/strong&gt;&amp;nbsp;skips your server completely and lets the client upload directly to storage.&lt;/p&gt;

&lt;p&gt;Choose the approach based on your file sizes and infrastructure instead of waiting for a large upload to crash your container.&lt;/p&gt;

&lt;p&gt;A good first step is to replace any&amp;nbsp;&lt;code&gt;bytes&lt;/code&gt;&amp;nbsp;parameter that is still being used for real file uploads.&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt;&amp;nbsp;&lt;a href="https://blog.filestack.com/fastapi-upload-file-multipart-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>file</category>
      <category>upload</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>How to Upload JPG File on Mobile with Orientation, Size and Format Traps</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Wed, 09 Sep 2026 10:45:00 +0000</pubDate>
      <link>https://dev.to/ideradevtools/how-to-upload-jpg-file-on-mobile-with-orientation-size-and-format-traps-j8i</link>
      <guid>https://dev.to/ideradevtools/how-to-upload-jpg-file-on-mobile-with-orientation-size-and-format-traps-j8i</guid>
      <description>&lt;p&gt;Three common problems can show up in the same sprint. An avatar appears sideways on Android. A 12 MB photo takes too long to upload on a weak connection. And an iPhone photo fails validation because it isn’t actually a JPG.&lt;/p&gt;

&lt;p&gt;Most guides on how to upload JPG files on mobile stop once the file is selected. But that’s where the real problems often begin.&lt;/p&gt;

&lt;p&gt;The photo might arrive rotated, be much larger than needed, or turn out to be a different format. Each of these problems has a clear cause and a simple fix.&lt;/p&gt;

&lt;p&gt;To upload a JPG file on mobile, let users pick from camera or gallery, then handle the three traps that break JPG uploads: EXIF orientation (photos arriving sideways), oversized camera outputs (often 5–12MB), and format mismatches such as HEIC masquerading as JPG on iOS. Client-side resize plus server-side normalisation solves all three. Filestack converts, rotates, and compresses automatically on upload.&lt;/p&gt;

&lt;p&gt;This article looks at each problem separately and then shows how to handle all of them in one step. That way, you don’t have to fix each upload bug as it appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EXIF Orientation values 3, 6, and 8 mark a rotated capture. Ignore them, and the photo displays sideways or upside down.&lt;/li&gt;
&lt;li&gt;Phone cameras commonly output 5 to 12MB JPGs at 12 to 48 megapixels, far more than any screen needs to display.&lt;/li&gt;
&lt;li&gt;iOS saves photos as HEIC by default. A file named&amp;nbsp;&lt;code&gt;photo.jpg&lt;/code&gt;&amp;nbsp;is not proof that it holds JPG data.&lt;/li&gt;
&lt;li&gt;A canvas resize on the client fixes orientation and file size in one step, since the redrawn pixels come out upright.&lt;/li&gt;
&lt;li&gt;Server-side normalisation catches every client you don’t control, including third-party apps and old app versions still in the wild.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trap 1, The Sideways Photo (EXIF Orientation)
&lt;/h2&gt;

&lt;p&gt;Phone cameras don’t rotate the pixel data when you turn the phone. They save the image as captured and write a rotation instruction into the EXIF metadata instead. Most photo apps read that instruction and display the photo the right way up. Plenty of image libraries and browsers don’t, and that’s when a portrait selfie shows up lying on its side in your app.&lt;/p&gt;

&lt;p&gt;The Orientation tag can hold several values, but three of them cause almost every sideways bug: values 3, 6, and 8 mark a rotated capture, corresponding to 180, 90, and 270 degrees. If your upload pipeline ignores this tag, the photo saves and displays exactly as rotated.&lt;/p&gt;

&lt;p&gt;The fix is to draw the image onto a canvas using the correct rotation before you upload it. Once it’s drawn, the pixels themselves are upright, so no downstream viewer can get it wrong again.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function drawUprightImage(file) {
return new Promise((resolve) =&amp;gt; {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) =&amp;gt; {
img.onload = () =&amp;gt; {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
getOrientation(file, (orientation) =&amp;gt; {
const swap = orientation &amp;gt;= 5 &amp;amp;&amp;amp; orientation &amp;lt;= 8;
canvas.width = swap ? img.height : img.width;
canvas.height = swap ? img.width : img.height;
switch (orientation) {
case 3: ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height); break;
case 6: ctx.transform(0, 1, -1, 0, canvas.height, 0); break;
case 8: ctx.transform(0, -1, 1, 0, 0, canvas.width); break;
default: break;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) =&amp;gt; resolve(blob), 'image/jpeg', 0.9);
});
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Fixing orientation on the client helps, but it only covers the clients you control. Keep that in mind while we move to the next trap, which shares part of the same fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2, The 12MB Camera File
&lt;/h2&gt;

&lt;p&gt;A modern phone camera shoots at 12 to 48 megapixels and saves the result as a JPG in the 5 to 12MB range. Almost nothing in your app needs that much resolution. A profile photo displays at a few hundred pixels wide. Even a full-screen image rarely needs more than 2000 pixels on its longest edge.&lt;/p&gt;

&lt;p&gt;Uploading the full-size image uses extra time and data without giving you much benefit. It’s really a page-size problem, just happening during the upload.&lt;/p&gt;

&lt;p&gt;The same rule used to speed up image loading applies here too: resize the image to the size you actually need before uploading it.&lt;/p&gt;

&lt;p&gt;Resize on the client before the upload starts. Canvas resize also solves this in the same pass as the orientation fix above, since you’re already redrawing the image. Set a maximum dimension, scale the canvas to fit it, and export at a reasonable JPEG quality like 0.8 or 0.9. A 10MB original commonly comes out under 1MB with no visible loss on a phone screen.&lt;/p&gt;

&lt;p&gt;Resizing the image on the device takes care of the file size before upload. The next problem is a little trickier because the file may look completely normal at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3, HEIC in JPG Clothing
&lt;/h2&gt;

&lt;p&gt;iOS stores photos as HEIC by default, not JPG. Some pickers and share sheets hand the file over with a&amp;nbsp;&lt;code&gt;.jpg&lt;/code&gt;&amp;nbsp;extension anyway, or a name that looks like a JPG, while the actual bytes are still HEIC. A file extension is a label someone chose. It is not proof of what’s inside the file.&lt;/p&gt;

&lt;p&gt;Trusting the extension is how format bugs make it to production undetected in testing. The safest check is the byte signature at the start of the file, not the name. JPG files start with the bytes FF D8 FF. HEIC files carry a different signature entirely. Check the actual bytes, and convert if the signature doesn’t match what the extension claims.&lt;/p&gt;

&lt;p&gt;This is where image transformation pipelines can make things easier. A common question is how to resize, crop, watermark, or change an image format on the fly.&lt;/p&gt;

&lt;p&gt;Instead of processing the same image several times, you can use one&amp;nbsp;&lt;a href="https://www.filestack.com/products/transformations/" rel="noopener noreferrer"&gt;transformation URL&lt;/a&gt;&amp;nbsp;to handle multiple changes in a single request. Filestack can use the image’s EXIF data to correct its orientation, convert HEIC images to JPG or WebP, and compress the image in the same transformation chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6yq78eecfv37rn80nz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6yq78eecfv37rn80nz7.png" alt=" " width="799" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we’ve covered all three problems, the next question is where to handle each one in your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation, Web Form and Native
&lt;/h2&gt;

&lt;p&gt;On mobile web, image uploads usually start with a file input. A common question is how to add image uploads to a web form. You can use&amp;nbsp;&lt;code&gt;&amp;lt;input type="file" accept="image/*" capture="environment"&amp;gt;&lt;/code&gt;&amp;nbsp;to open the camera directly. If you remove&amp;nbsp;&lt;code&gt;capture&lt;/code&gt;, users can choose between the camera and gallery.&lt;/p&gt;

&lt;p&gt;Native apps work a little differently. Both iOS and Android have SDKs that can handle camera and gallery permissions, so you don’t have to build that flow yourself.&lt;/p&gt;

&lt;p&gt;React Native has its own approach. Image picker libraries return a local file URI instead of a browser File object, so your upload code needs to read the file from that URI first.&lt;/p&gt;

&lt;p&gt;The fixes for orientation and file size stay the same across platforms. Only the way you get the file changes.&lt;/p&gt;

&lt;p&gt;You can build all of this yourself, but that means maintaining similar upload logic for web, iOS, Android, and React Native. There is a simpler way to handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route, Normalise on Ingestion
&lt;/h2&gt;

&lt;p&gt;You can handle all three problems with a single&lt;a href="https://www.filestack.com/products/file-upload/" rel="noopener noreferrer"&gt;&amp;nbsp;mobile file upload&lt;/a&gt;&amp;nbsp;flow that automatically fixes orientation, converts formats, and compresses images as they are uploaded.&lt;/p&gt;

&lt;p&gt;Instead of writing separate code for image rotation on the web, native SDKs, and HEIC checks, you can use one transformation process for every file as soon as it arrives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wjn7z7ehki9cr9vjlyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wjn7z7ehki9cr9vjlyv.png" alt=" " width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Server-side normalisation also handles files from places you don’t control. Older app versions, third-party integrations, and API requests might skip your client-side fixes. A server-side step catches these files too and makes sure they follow the same rules.&lt;/p&gt;

&lt;p&gt;Once the file is normalised, you can create different image sizes for your app. This leads to another common question: how can you generate thumbnails automatically after an upload?&lt;/p&gt;

&lt;p&gt;You can create thumbnails, medium previews, and full-size versions from the same normalised image. This is much simpler than running a separate resize job for each version.&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ve covered how to fix each problem. Here’s a quick summary to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Trust Bytes, Not Extensions
&lt;/h2&gt;

&lt;p&gt;Three traps, three fixes: orient from EXIF instead of trusting the file as captured, resize early instead of uploading the full camera output, and convert by byte signature instead of trusting the file extension. Client-side resize handles the traffic you can see. Server-side normalisation catches everything else.&lt;/p&gt;

&lt;p&gt;Run a real phone photo through a transformation sandbox and check all three: does it come out upright, does it come out at a sane file size, and does it come out as an actual JPG regardless of what the original claimed to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why do my mobile photo uploads appear sideways?
&lt;/h2&gt;

&lt;p&gt;EXIF Orientation is being ignored somewhere in the pipeline. Auto-orient on ingestion, or draw the image upright client-side using the Orientation tag before upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did an iPhone JPG upload fail validation?
&lt;/h2&gt;

&lt;p&gt;It was likely HEIC with a JPG-style name. Check the byte signature rather than the file extension, and convert HEIC to JPG or WebP if the signature doesn’t match.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big are phone camera JPGs?
&lt;/h2&gt;

&lt;p&gt;Commonly 5 to 12MB at 12 to 48 megapixels. Resize to the display target before or during upload rather than sending the original file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt;&amp;nbsp;&lt;a href="https://blog.filestack.com/upload-jpg-file-mobile-traps/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>upload</category>
      <category>jpg</category>
    </item>
    <item>
      <title>How to Build a Scalable Image Upload Component with Shadcn UI</title>
      <dc:creator>IderaDevTools</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:10:02 +0000</pubDate>
      <link>https://dev.to/ideradevtools/how-to-build-a-scalable-image-upload-component-with-shadcn-ui-198o</link>
      <guid>https://dev.to/ideradevtools/how-to-build-a-scalable-image-upload-component-with-shadcn-ui-198o</guid>
      <description>&lt;p&gt;If you search for a Shadcn UI image upload component, you’ll quickly notice there isn’t one. That’s intentional.&lt;/p&gt;

&lt;p&gt;You might try running&amp;nbsp;&lt;code&gt;npx shadcn add upload&lt;/code&gt;, only to find that no upload component exists. Then you end up on the same GitHub discussions as many other developers asking the same question: Where is the image upload component?&lt;/p&gt;

&lt;p&gt;The honest answer is: you build it yourself.&lt;/p&gt;

&lt;p&gt;At first, that might seem surprising, but it actually makes sense. Shadcn UI isn’t a library where you install ready-made components. Instead, it gives you building blocks that you add to your own codebase and customise as needed.&lt;/p&gt;

&lt;p&gt;Image upload is different for every application. The upload flow, states, and backend integration can vary a lot, so a single upload component wouldn’t work for every use case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shadcn UI ships no built-in image upload component; you compose one from its primitives (Button, Card, Progress, Dialog) around an upload engine that handles files, previews, and errors. The clean split is shadcn for presentation and a dedicated uploader for transfer. Filestack’s React SDK slots in as that engine, keeping the shadcn look while adding chunked, resumable uploads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this guide, we’ll build an image upload component step by step. We’ll use Shadcn UI to create the interface, add state management and validation, and then connect it to an upload service.&lt;/p&gt;

&lt;p&gt;The best part is that you can change the upload provider later without rebuilding the UI. The interface stays the same while the upload logic can be swapped whenever you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Shadcn/ui has no upload component on purpose. You compose one from Card, Button, Progress, and Dialog.&lt;/li&gt;
&lt;li&gt;A working upload UI needs four pieces: a dropzone surface, file rows, a progress bar per row, and error text inside the row, not a toast.&lt;/li&gt;
&lt;li&gt;A hidden&amp;nbsp;&lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt;&amp;nbsp;paired with a&amp;nbsp;&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;&amp;nbsp;keeps the dropzone accessible and keyboard operable.&lt;/li&gt;
&lt;li&gt;Keep transfer logic behind a small interface. Swapping&amp;nbsp;&lt;code&gt;fetch()&lt;/code&gt;&amp;nbsp;for a real upload SDK should change zero markup.&lt;/li&gt;
&lt;li&gt;Filestack’s React SDK maps its progress callbacks directly onto Shadcn’s&amp;nbsp;&lt;code&gt;&amp;lt;code&amp;gt;Progress&lt;/code&gt;&amp;nbsp;component, so the engine and the UI stay decoupled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before building anything, it’s worth understanding why this gap exists, because it shapes every decision after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Shadcn Does Not Ship an Uploader
&lt;/h2&gt;

&lt;p&gt;Shadcn’s whole philosophy is “copy the code, own the code.” That works well for a button or a dialog, because those components don’t hold much internal state. An uploader is different. It has to track file selection, per-file progress, retries, validation errors, and network failures, all at once. Trying to template that into one drop-in component would mean baking in assumptions about your backend, your file size limits, and your error handling, exactly the kind of lock-in shadcn tries to avoid.&lt;/p&gt;

&lt;p&gt;So instead of asking “what are the best React components for file uploading,” the more useful question becomes “which primitives do I already have, and what’s missing?” As it turns out, you already have most of what you need. What’s missing is the part that actually talks to a server.&lt;/p&gt;

&lt;p&gt;With that context in place, let’s start stacking primitives into an actual dropzone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composing the Surface: Dropzone plus Rows plus Progress
&lt;/h2&gt;

&lt;p&gt;This is where shadcn earns its keep. A React drag-and-drop file upload surface and a React file upload component for the file list are really the same composition problem, just two different views of it.&lt;/p&gt;

&lt;p&gt;The dropzone itself is a Card wrapping a hidden file input and a label. The label pattern matters here: clicking anywhere on the label opens the file picker, and because it’s a real form control under the hood, keyboard users can tab to it and hit Enter or Space to open it too. Below the dropzone, each selected file becomes its own row, and each row gets its own Progress bar.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Card } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
export function ImageUploader({ files, onFilesSelected, onRetry }) {
return (
&amp;lt;Card className="p-6"&amp;gt;
&amp;lt;label
htmlFor="file-input"
className={cn(
"flex flex-col items-center justify-center gap-3",
"rounded-lg border-2 border-dashed border-muted-foreground/30",
"py-10 text-center cursor-pointer hover:border-primary/50"
)}
&amp;gt;
&amp;lt;span className="font-medium"&amp;gt;Drag and drop images here&amp;lt;/span&amp;gt;
&amp;lt;span className="text-sm text-muted-foreground"&amp;gt;or click to browse&amp;lt;/span&amp;gt;
&amp;lt;input
id="file-input"
type="file"
multiple
accept="image/*"
className="sr-only"
onChange={(e) =&amp;gt; onFilesSelected(Array.from(e.target.files))}
/&amp;gt;
&amp;lt;/label&amp;gt;
&amp;lt;ul className="mt-6 space-y-3"&amp;gt;
{files.map((file) =&amp;gt; (
&amp;lt;li key={file.id} className="rounded-md border p-3"&amp;gt;
&amp;lt;div className="flex items-center justify-between text-sm"&amp;gt;
&amp;lt;span className="font-medium"&amp;gt;{file.name}&amp;lt;/span&amp;gt;
{file.status === "failed" ? (
&amp;lt;button
onClick={() =&amp;gt; onRetry(file.id)}
className="text-destructive underline"
&amp;gt;
Retry
&amp;lt;/button&amp;gt;
) : (
&amp;lt;span className="text-muted-foreground"&amp;gt;{file.status}&amp;lt;/span&amp;gt;
)}
&amp;lt;/div&amp;gt;
&amp;lt;Progress value={file.progress} className="mt-2 h-2" /&amp;gt;
{file.error &amp;amp;&amp;amp; (
&amp;lt;p className="mt-1 text-xs text-destructive"&amp;gt;{file.error}&amp;lt;/p&amp;gt;
)}
&amp;lt;/li&amp;gt;
))}
&amp;lt;/ul&amp;gt;
&amp;lt;/Card&amp;gt;
);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26zmhgk8am619hjzlfea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26zmhgk8am619hjzlfea.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the error text sits inside the row, right under that file’s own progress bar, instead of floating away in a toast. A toast disappears in a few seconds. A row stays put until the user deals with it, which matters when three out of twenty files failed, and you don’t want the person hunting for which ones.&lt;/p&gt;

&lt;p&gt;The surface is only half the job, though. Right now, none of this actually tracks state. Let’s fix that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  State and Validation
&lt;/h2&gt;

&lt;p&gt;With the JSX in place, the component needs somewhere to keep track of what’s happening to each file, and a way to say no to files that shouldn’t be there in the first place.&lt;/p&gt;

&lt;p&gt;A&amp;nbsp;&lt;code&gt;useReducer&lt;/code&gt;&amp;nbsp;keyed by file ID works well here, since every file’s status changes independently of the others. Validation, checking file type and size, happens the moment a file is selected, before any request goes out. This answers a common early question too: file uploading in React JS almost always starts with this same shape, a reducer plus a validation step, no matter which transport ends up sending the bytes.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function uploadReducer(state, action) {
switch (action.type) {
case "ADD_FILES":
return {
...state,
...Object.fromEntries(
action.files.map((f) =&amp;gt; [f.id, { ...f, status: "queued", progress: 0 }])
),
};
case "PROGRESS":
return {
...state,
[action.id]: { ...state[action.id], status: "uploading", progress: action.pct },
};
case "DONE":
return { ...state, [action.id]: { ...state[action.id], status: "done", progress: 100 } };
case "ERROR":
return {
...state,
[action.id]: { ...state[action.id], status: "failed", error: action.message },
};
default:
return state;
}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Validation lives right where files enter the component:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function validateFile(file) {
if (!file.type.startsWith("image/")) return "Only image files are allowed.";
if (file.size &amp;gt; 10 * 1024 * 1024) return "File is larger than 10MB.";
return null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Most guides on implementing image uploads in React stop here, with a working component and simulated progress. That’s enough to show how the UI works.&lt;/p&gt;

&lt;p&gt;But in a real application, you also need something that uploads the file and reports the actual upload progress. That’s where choosing the right upload solution becomes important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engine Swap: fetch to SDK
&lt;/h2&gt;

&lt;p&gt;This is the piece that makes the whole composition worth the extra setup: the transport layer sits behind a small interface, so the shadcn component above never needs to know or care how bytes actually get to the server.&lt;/p&gt;

&lt;p&gt;Start with the interface itself. It only needs one method, and it only needs to report progress and completion:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// uploadEngine.js
export function createFetchEngine(endpoint) {
return {
upload(file, { onProgress, onDone, onError }) {
const xhr = new XMLHttpRequest();
const form = new FormData();
form.append("file", file);
xhr.upload.onprogress = (e) =&amp;gt; {
onProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () =&amp;gt; (xhr.status &amp;lt; 300 ? onDone(xhr.response) : onError("Upload failed"));
xhr.onerror = () =&amp;gt; onError("Network error");
xhr.open("POST", endpoint);
xhr.send(form);
},
};
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A React JS file upload component built this way already works. The catch is that raw fetch or&amp;nbsp;&lt;code&gt;XMLHttpRequest&lt;/code&gt;&amp;nbsp;gives you one request per file, no chunking, and no way to resume a large upload that drops halfway through. Swapping in&lt;a href="https://www.filestack.com/sdks/react/" rel="noopener noreferrer"&gt;&amp;nbsp;Filestack’s React SDK&lt;/a&gt;&amp;nbsp;as the engine keeps the exact same interface, but the internals now handle chunked, resumable transfer:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { init } from "filestack-js";
export function createFilestackEngine(apiKey) {
const client = init(apiKey);
return {
upload(file, { onProgress, onDone, onError }) {
client
.upload(file, {
onProgress: (evt) =&amp;gt; onProgress(Math.round(evt.totalPercent)),
})
.then((res) =&amp;gt; onDone(res))
.catch((err) =&amp;gt; onError(err.message));
},
};
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1buhbe7s6ixjb0cjw9o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1buhbe7s6ixjb0cjw9o.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both upload engines use the same&amp;nbsp;&lt;code&gt;upload(file, callbacks)&lt;/code&gt;&amp;nbsp;function, so you don’t need to change the component from Section 2. The only thing that changes is the upload engine you pass into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Managed Route: Shadcn Look, Production Engine
&lt;/h2&gt;

&lt;p&gt;Building your own fetch-based engine is a fine way to learn the shape of the problem, and it’s genuinely enough for a small internal tool. But once you need chunking for large images, resumable uploads on flaky connections, or reliable retry behaviour, that engine starts asking for real maintenance time.&lt;/p&gt;

&lt;p&gt;Keep the markup, upgrade the engine: wire the composed component to a production&lt;a href="https://www.filestack.com/products/file-upload/" rel="noopener noreferrer"&gt;&amp;nbsp;upload ui&lt;/a&gt;&amp;nbsp;and the same shadcn rows gain chunked transfer, retries, and 5GB file support, without a redesign. The&amp;nbsp;&lt;code&gt;onProgress&lt;/code&gt;&amp;nbsp;callback from the React SDK maps one-to-one onto the Progress value you’re already rendering, so the swap really is as small as it looks in the code above.&lt;/p&gt;

&lt;p&gt;This is also where teams building something like a profile picture uploader tend to land. The UI stays identical to a plain gallery upload, same dropzone, same rows, but the failure modes that matter for a single, important image (say, someone’s profile photo) get handled by the engine instead of a hand-rolled retry loop.&lt;/p&gt;

&lt;p&gt;If you’re curious how that plays out for single-file, high-stakes uploads, our&lt;a href="https://blog.filestack.com/file-upload-react-easy-tutorial/" rel="noopener noreferrer"&gt;&amp;nbsp;React file upload walkthrough&lt;/a&gt;&amp;nbsp;covers that shape in more depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Own the Pixels, Outsource the Packets
&lt;/h2&gt;

&lt;p&gt;The lesson underneath all of this is simpler than it looks in the code: shadcn was never going to ship an uploader, because uploading isn’t really a presentation problem. It’s a transport problem wearing a UI.&lt;/p&gt;

&lt;p&gt;Split the two apart, and both sides get easier. You keep full control over how the dropzone and rows look and feel, since that’s just your own JSX and Tailwind classes. And you keep the option to swap the engine underneath, from a quick fetch call to something built for chunking and retries, without ever touching that markup again.&lt;/p&gt;

&lt;p&gt;If you want to see it end to end, copy the composed component above and connect it to Filestack to see real progress values fill in those same progress bars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This article was published on the&lt;/em&gt;&lt;/strong&gt;&amp;nbsp;&lt;a href="https://blog.filestack.com/shadcn-ui-image-upload-component/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Filestack blog&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>filestack</category>
      <category>image</category>
      <category>upload</category>
    </item>
  </channel>
</rss>
