PageSourceSearch

https://shakacode.com/component---src-template-blog-article-tsx-co…-pipeline-index-mdx-07435d5ccd322ad805f4.js

js shakacode.com collected 2026-10-04 00:25:59 UTC 19,188 bytes, 2 lines download raw bytes

1"use strict";(self.webpackChunkgatsby_starter_default=self.webpackChunkgatsby_starter_default||[]).push([[1464],{58084:function(e,t,a){a.r(t),a.d(t,{Head:function(){return w},default:function(){return y}});var n=a(28453),r=a(96540);function i(e){const t=Object.assign({p:"p",strong:"strong",span:"span",ul:"ul",li:"li",h2:"h2",a:"a",em:"em"},(0,n.R)(),e.components);return r.createElement(r.Fragment,null,r.createElement(t.p,null,"The wrong way to leave a four-figure Cloudinary bill is to replace Cloudinary."),"\n",r.createElement(t.p,null,"That was the tempting move, but the account held about 1 TB of originals and other properties still depended on it - moving the whole library is a migration project with a large blast radius. The better question is narrower: how do we stop paying Cloudinary to transform and deliver the same pixels over and over, while keeping it as the place those originals live?"),"\n",r.createElement(t.p,null,"We designed a small image pipeline for a vacation-rental listings marketplace around that question: a self-hosted scaler behind the Cloudflare CDN already in use, with Cloudflare R2 as an originals mirror, leaving Cloudinary as the system of record. Expected recurring cost: about ",r.createElement(t.strong,null,"$65-70 per month"),", down from a four-figure bill."),"\n",r.createElement(t.p,null,"The interesting part is not putting ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sharp</code>'}})," on a VM - anyone can do that. Two subtler details make the economics hold, and they are the ideas I would carry into any image-pipeline redesign:"),"\n",r.createElement(t.ul,null,"\n",r.createElement(t.li,null,"R2 makes Cloudinary egress a one-time fetch per original, not a recurring cache-miss tax."),"\n",r.createElement(t.li,null,"The compute is sized for memory during cold request packs, not for average transforms per second."),"\n"),"\n",r.createElement(t.h2,{id:"cloudinary-was-not-the-thing-to-remove",style:{position:"relative"}},r.createElement(t.a,{href:"#cloudinary-was-not-the-thing-to-remove","aria-label":"cloudinary was not the thing to remove permalink",className:"anchor before"},r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Cloudinary was not the thing to remove"),"\n",r.createElement(t.p,null,"The account was already in better shape than its bill suggested. The app had normalized image URLs to deterministic ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">w</code>'}}),", ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">h</code>'}}),", ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">q</code>'}})," transforms, and Cloudflare sat in front with Tiered Cache and Cache Reserve, so most derivatives lived durably at the edge. What stayed expensive was Cloudinary's per-transformation pricing (about 137k transforms/day, ~4.1M/month) and the bytes pulled from Cloudinary on origin misses. So the design keeps what Cloudinary is good at - durable original storage and the upload workflow - and moves what is expensive at this volume: producing derivatives and serving origin misses."),"\n",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<figure class="gatsby-resp-image-figure" style="">\n    <span\n      class="gatsby-resp-image-wrapper"\n      style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 750px; "\n    >\n      <a\n    class="gatsby-resp-image-link"\n    href="/static/7e087c539f61dfe5151f7704c4135449/555cf/image-1.png"\n    style="display: block"\n    target="_blank"\n    rel="noopener"\n  >\n    <span\n    class="gatsby-resp-image-background-image"\n    style="padding-bottom: 31.382978723404253%; position: relative; bottom: 0; left: 0; background-image: url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+0lEQVR42mVQ0W7EIAzr/3/bpL1s0nSn3ba7clBaCrRQCCR0Ya9DJrIiO5Y8tNYASko5A/QPUGvlWUrJOf9NSAmI6Pz3BjaHEFlRSmVFzFVoK2dn/OFjcbGExHtMu0n7QnnD5BkEW6M6sOEhnno2W4h8yAV4ebu9vt8+x0UscXLgYiWserzI+wd4Ga1I7knH3DD35G3bF7Mas3q/zYZj7X70QIaZZvkY3aSSUbibHH3wqxQ/3s6tYTezUyqtWKe0lJNU05EyIjbCoIS4XvTXzT9FtvZIYP3+fR/1Yqm1bq4VkagiMWHaUbFXxtWVwlewk15h6VLkqth1nucvGY1XoD/ohuoAAAAASUVORK5CYII=\'); background-size: cover; display: block;"\n  ></span>\n  <img\n        class="gatsby-resp-image-image"\n        alt="Cloudflare serves; on a miss the scaler resizes and reads from the R2 mirror, hitting Cloudinary once per original"\n        title=""\n        src="/static/7e087c539f61dfe5151f7704c4135449/1d69c/image-1.png"\n        srcset="/static/7e087c539f61dfe5151f7704c4135449/4dcb9/image-1.png 188w,\n/static/7e087c539f61dfe5151f7704c4135449/5ff7e/image-1.png 375w,\n/static/7e087c539f61dfe5151f7704c4135449/1d69c/image-1.png 750w,\n/static/7e087c539f61dfe5151f7704c4135449/78797/image-1.png 1125w,\n/static/7e087c539f61dfe5151f7704c4135449/aa440/image-1.png 1500w,\n/static/7e087c539f61dfe5151f7704c4135449/555cf/image-1.png 1960w"\n        sizes="(max-width: 750px) 100v
1w, 750px"\n        style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;"\n        loading="lazy"\n        decoding="async"\n      />\n  </a>\n    </span>\n    <figcaption class="gatsby-resp-image-figcaption">Cloudflare serves; on a miss the scaler resizes and reads from the R2 mirror, hitting Cloudinary once per original</figcaption>\n  </figure>'}}),"\n",r.createElement(t.p,null,"The traffic path is deliberately boring. A user requests a derivative through Cloudflare; on an edge miss, nginx sends it to an in-house scaler that resizes with sharp/libvips (the same engine family as Cloudinary). Cloudflare still owns the derivative cache, so the VM needs no big disk of generated files - its small nginx cache exists only so ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">proxy_cache_lock</code>'}})," collapses a burst of identical misses into one fetch-and-resize instead of a stampede."),"\n",r.createElement(t.h2,{id:"the-r2-mirror-is-the-whole-trick",style:{position:"relative"}},r.createElement(t.a,{href:"#the-r2-mirror-is-the-whole-trick","aria-label":"the r2 mirror is the whole trick permalink",className:"anchor before"},r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"The R2 mirror is the whole trick"),"\n",r.createElement(t.p,null,"Flat-rate compute removes metered transformations, but it does not solve origin economics. Without a mirror, every derivative cache miss can still reach Cloudinary - a local cache only helps until the disk fills, the instance is replaced, or a new size or format is requested. That is not a durable bound on egress."),"\n",r.createElement(t.p,null,"R2 changes the shape of the problem. On a miss the scaler checks R2 first; egress from R2 to the compute host is free. If the original is not there, it fetches from Cloudinary ",r.createElement(t.strong,null,"once"),", writes it to R2, and every later request - any thumbnail, any width, any format - starts from R2. A Cloudinary fetch is tied to the unique original, not to a derivative key. That buys three kinds of resilience: Cache Reserve can evict a derivative without reopening Cloudinary egress, a VM can be rebuilt without losing the originals cache, and a new ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">srcset</code>'}})," width can fan out without paying Cloudinary again. After a lazy warm-up - we fill R2 only as traffic asks for an asset, never a bulk copy - Cloudinary egress approaches zero except for genuinely new uploads."),"\n",r.createElement(t.h2,{id:"average-cpu-would-buy-the-wrong-box",style:{position:"relative"}},r.createElement(t.a,{href:"#average-cpu-would-buy-the-wrong-box","aria-label":"average cpu would buy the wrong box permalink",className:"anchor before"},r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Average CPU would buy the wrong box"),"\n",r.createElement(t.p,null,"Divide the volume by time and it looks calm: ~137k transforms/day is roughly 1.6 per second, and the scaler only sees Cloudflare misses, so its real rate is at or below that. A sister site runs the same scaler at 1-2 cores. A small CPU-focused box looks obvious - and it is the wrong sizing model."),"\n",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<figure class="gatsby-resp-image-figure" style="">\n    <span\n      class="gatsby-resp-image-wrapper"\n      style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 750px; "\n    >\n      <a\n    class="gatsby-resp-image-link"\n    href="/static/1c91a6bfb67176b6089ce537c421ec5a/555cf/image-2.png"\n    style="display: block"\n    target="_blank"\n    rel="noopener"\n  >\n    <span\n    class="gatsby-resp-image-background-image"\n    style="padding-bottom: 23.404255319148938%; position: relative; bottom: 0; left: 0; background-image: url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAIAAADKYVtkAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAy0lEQVR42nWP207EMAxE8/+/xguwomwlCguLoNv0kqTOxrGdlKR9ZnRkjcaWpVHMwpIkpchMLEQcqRqWmlNZS9r+kQI7OTOCm/HuorcRkRDXeSrAstytZQ9bSpKzhWDWUGbBY/mc1bV76c5Pl7eT0dfkhywoIXy154+mKbj+dwObMUCUU/v58Nw+vr433ffNko+ivLl5M8DSr3NPMAqhRPSjXvVQQLOIMyliiNJP8KOdNsEAz0CRkto7V3HVMQ9TPR3QHtUb2S0fnf8A8TUeqzDjRXIAAAAASUVORK5CYII=\'); background-size: cover; display: block;"\n  ></span>\n  <img\n        class="gatsby-resp-image-image"\n        alt="A concurrency cap keeps peak memory bounded no matter how large the cold request pack"\n        title=""\n        src="/static/1c91a6bfb67176b6089ce537c421ec5a/1d69c/image-2.png"\n        srcset="/static/1c91a6bfb67176b6089ce537c421ec5a/4dcb9/image-2.png 188w,\n/static/1c91a6bfb67176b6089ce537c421ec5a/5ff7e/image-2.png 375w,\n/static/1c91a6bfb67176b6089ce537c421ec5a/1d69c/image-2.png 750w,\n/static/1c91a6bfb67176b6089ce537c421ec5a/78797/image-2.png 1125w,\n/static/1c91a6bfb67176b6089ce537c421ec5a/aa440/image-2.png 1500w,\n/static/1c91a6bfb67176b6089ce537c421ec5a/555cf/image-2.png 1960w"\n        sizes="(max-width: 750px) 100v
1w, 750px"\n        style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;"\n        loading="lazy"\n        decoding="async"\n      />\n  </a>\n    </span>\n    <figcaption class="gatsby-resp-image-figcaption">A concurrency cap keeps peak memory bounded no matter how large the cold request pack</figcaption>\n  </figure>'}}),"\n",r.createElement(t.p,null,"Image traffic does not arrive as an average. A gallery's ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">srcset</code>'}})," or a crawler sweep creates a ",r.createElement(t.em,null,"pack")," of cold misses at once, each starting a 2-3k px decode that holds ~50-120 MB in libvips. The risk is not a CPU graph at 100%; it is a synchronized pack eating memory faster than a small box can survive. The fix is a concurrency cap - a semaphore and queue that make peak memory independent of burst size. That drives a 16 GB RAM floor (the recommended baseline is two 8 GB nodes for ~$48-50/month, high-availability from day one). The lesson: size for the simultaneous decodes that hurt you, then cap them. Do not buy cores to solve a memory burst."),"\n",r.createElement(t.p,null,"Add R2 storage for 1 TB of originals ($15/month) and a few dollars of operations, and steady-state Cloudinary and Cloudflare egress near zero, and the recurring total lands at about ",r.createElement(t.strong,null,"$65-70/month")," - fixed compute plus cheap durable storage, instead of a per-request meter."),"\n",r.createElement(t.h2,{id:"the-cloudflare-native-options-we-rejected-on-cost",style:{position:"relative"}},r.createElement(t.a,{href:"#the-cloudflare-native-options-we-rejected-on-cost","aria-label":"the cloudflare native options we rejected on cost permalink",className:"anchor before"},r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"The Cloudflare-native options we rejected on cost"),"\n",r.createElement(t.p,null,"We first looked at keeping the whole path inside Cloudflare. Both metered options were a bad fit at this volume:"),"\n",r.createElement(t.ul,null,"\n",r.createElement(t.li,null,r.createElement(t.strong,null,"Cloudflare Image Transformations (~$650-2,000/mo)")," - the same per-transform metered model we were trying to leave."),"\n",r.createElement(t.li,null,r.createElement(t.strong,null,"Cloudflare Containers (~$586/mo, al
1ways on)")," - built for bursty, scale-to-zero work, not a warm steady resizer."),"\n",r.createElement(t.li,null,r.createElement(t.strong,null,"Hetzner block-volume cache (~$60/TB/mo)")," - an expensive bulk store; R2 keeps the originals at $15/TB with free read-back."),"\n"),"\n",r.createElement(t.p,null,"There is also a hard technical wall: ",r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<code class="language-text">sharp</code>'}})," cannot run in Cloudflare Workers. It needs native libvips binaries, and Workers are V8 isolates with no native code. WASM resizers exist, but the 128 MB Workers memory limit is a bad match for 2-3k px originals, and per-request billing returns through another door. None of this is anti-Cloudflare - Cloudflare stays the delivery layer and durable derivative cache. It is an argument for putting native image work on flat-cost compute and using the right storage primitive for originals."),"\n",r.createElement(t.h2,{id:"why-it-was-cheap-to-build",style:{position:"relative"}},r.createElement(t.a,{href:"#why-it-was-cheap-to-build","aria-label":"why it was cheap to build permalink",className:"anchor before"},r.createElement(t.span,{dangerouslySetInnerHTML:{__html:'<svg aria-hidden="true" height="20" version="1.1" viewBox="0 0 16 16" width="20"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'}})),"Why it was cheap to build"),"\n",r.createElement(t.p,null,"The scaler already exists and runs on a sister site, so this was configuration, integration, and safety work - not a new platform. The estimate was 30-55 hours: adapt the scaler, add the R2 mirror, support legacy URL conversion, enforce the concurrency cap, provision the nodes, cut Cloudflare over, and load-test the request-pack path (the one test that actually matters, because that is where the failure mode lives)."),"\n",r.createElement(t.p,null,"Cloudinary stays in place throughout - no big-bang migration, no new upload system. That is the pattern I would reuse: keep the durable system of record, put transformation work on predictable compute, make the origin mirror durable enough that cache loss cannot bring the old bill back, and cap concurrency before real traffic finds the edge of your RAM."),"\n",r.createElement(t.p,null,"Aloha,\nSergey"))}var o=function(e){void 0===e&&(e={});const{wrapper:t}=Object.assign({},(0,n.R)(),e.components);return t?r.createElement(t,e,r.createElement(i,e)):i(e)},s=a(5588),l=a(65427),c=a(20504),d=a(43397),m=a(61942);const h=e=>e.replace(/\/+$/,""),g=(e,t)=>t?""+e+(t.startsWith("/")?t:"/"+t):e,u=(e,t)=>g(h(e),t),p=(e,t)=>g((e=>"undefined"!=typeof window?window.location.origin:h(e))(e),t),f=(e,t)=>{const a=(0,s.d)(t);return a?u(e,a):(e=>u(e,"/shakacode-og.png"))(e)},b=e=>{let{data:t,location:a,children:n}=e;const{frontmatter:i}=t.mdx,{siteUrl:o}=t.site.siteMetadata,s=p(o,a.pathname);return r.createElement(r.Fragment,null,r.createElement(l.A,null),r.createElement(c.A,{header:r.createElement(m.r,{breadcrumb:i.title}),main:r.createElement(r.Fragment,null,r.createElement(m.c,{body:n,title:i.title,author:i.author,date:i.date,link:s,categories:i.categories,featuredImage:i.featuredImage,showFeaturedImage:i.showFeaturedImage}),r.createElement(d.LN,null))}))};function y(e){return r.createElement(b,e,r.createElement(o,e))}const w=e=>{let{data:t,location:a}=e;const{frontmatter:n,excerpt:i}=t.mdx,{siteMetadata:o}=t.site,{siteUrl:s,title:l,description:c}=o,d=((e,t,a)=>e.excerpt||t||a)(n,i,c),m=f(s,n.featuredImage),h=u(s,a.pathname);return r.createElement(r.Fragment,null,r.createElement("title",null,n.title+" | "+l),r.createElement("link",{rel:"canonical",href:h}),r.createElement("meta",{name:"description",content:d}),r.createElement("meta",{property:"og:title",content:n.title}),r.createElement("meta",{property:"og:description",content:d}),r.createElement("meta",{property:"og:type",content:"article"}),r.createElement("meta",{property:"og:url",content:h}),r.createElement("meta",{property:"og:image",content:m}),r.createElement("meta",{name:"image",content:m}),r.createElement("meta",{name:"twitter:card",content:"summary_large_image"}),r.createElement("meta",{name:"twitter:title",content:n.title}),r.createElement("meta",{name:"twitter:description",content:d}),r.createElement("meta",{name:"twitter:image",content:m}))}}}]);
2//# sourceMappingURL=component---src-template-blog-article-tsx-content-file-path-src-posts-cloudinary-to-self-hosted-image-pipeline-index-mdx-07435d5ccd322ad805f4.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.