A college student in Bangalore browsing on 5G with an iPhone 15. A homemaker in Siwan, Bihar, scrolling through sarees on a Redmi 9A with 2GB of RAM over patchy 3G. A first-time buyer in Tinsukia, Assam, on a JioPhone Next with a 5.5-inch 720p screen and a signal that drops every few minutes. A reseller in Ajmer comparing product photos on a Samsung M13 over 4G that barely holds three bars indoors. They all open Meesho, and they all deserve a product photo that loads fast, looks sharp, and doesn't eat their data plan. Pixius is the image service that makes this possible by serving the right size, the right format, and the right quality for every request, transforming images on the fly so no user pays the cost of a one-size-fits-all approach.
The Problem: One Image, A Thousand Ways to Serve It
Meesho's mission is to democratize internet commerce for everyone. But democratization means nothing if half your users can't load a product image because it's a 2MB JPEG optimized for a desktop monitor.
Now consider the math. A single product photo needs to be available in multiple widths (from 64px thumbnails to 1024px full-size), multiple formats (JPEG, WebP, PNG), multiple quality levels, and multiple crops. The combinatorial explosion is real, dozens of variants per image, millions of images, and pre-generating every combination is wasteful because most will never be requested.
Instead of creating every possible version of an image in advance (which wastes storage space on versions no one will ever use), we create each version only when someone actually asks for it. Once a version is created, we save it. This means popular images are always ready to serve, while we avoid the work of creating millions of unused variations.
The Cloud Run That Got Us Here
To understand what we built, you need to see what came before and why we outgrew it.
The predecessor was a Cloud Run instance - lean, simple, and honest about what it did. You handed it an image and some dimensions, it handed you back a resized JPEG or WebP. Resize and reformat was its entire vocabulary. For a while, that was fine. Most good systems start this way: small scope, clear contract, nobody complaining.
But even within that small scope, things were fragile in ways we didn't notice until they bit us. Transformed images were stored right next to originals, distinguished only by a suffix tacked onto the filename - sneaker_720.jpg, sneaker_1024.jpg. The naming scheme didn't distinguish between originals and transformed variants, a gap that motivated the deterministic path design in Pixius.
Then there was how the code actually handled requests. It followed a logical, linear flow: check if the source image exists, apply the transformation, serve the result. Straightforward to read, easy to reason about and deeply inefficient at scale. For the overwhelming majority of requests, the transformed variant was already sitting in storage, generated hours or days ago. But the code didn't know that. It always starts from the beginning – verify the source, then look for the transform. That redundant first check meant an extra storage round-trip on every request, adding latency the user could feel and blob storage operation costs that quietly inflated the bill.
The most efficient flow would check for the transformed image first and skip the source lookup entirely when the variant already existed. But the code wasn't written for efficiency at scale, it was written for correctness in the simple case, and those are very different things.
As Meesho grew, so did the demands on this simple service. Requirements started arriving that went well beyond basic resizing, teams wanted to experiment with new image formats, tune quality settings for different surfaces, apply crops for specific aspect ratios. And the serving side was only half the problem. Every team had their own way of getting images into storage – some uploading directly through the blob storage console, some using client libraries, some going through generic file uploader services. Nobody had a unified view of what was being stored where, how storage was growing, or what was accessed across these ever-expanding buckets. We needed a centralized image platform – one that could handle serving, transforming, uploading, access control, and auditing under one roof. And that's how the idea of Pixius came to be.
Why Transformations Matter: The Physics of Real Devices
Serving the original upload is almost never the right answer.
Width and viewport reality. A seller uploads a product photo at 2048px wide. A flagship phone needs at most ~720px (device pixel ratio on a ~360px CSS viewport). A budget Android needs ~360px. A thumbnail grid needs 128px. Serving the full 2048px to any of these wastes bandwidth on pixels the screen cannot render and the browser still decodes all 2048px into memory (~16MB in raw bitmap) before throwing 75%+ away during downscaling.
Format and bandwidth reality. JPEG is a 30-year-old format. WebP delivers 25-30% smaller files at equivalent visual quality; AVIF pushes that to 40-50%. For a user on 3G in a tier-2 Indian city, the difference between a 200KB JPEG and a 100KB AVIF is 2-second versus 5-second load times. But format selection isn't "always use the newest codec" – AVIF encoding is ~5-10x more CPU-intensive than JPEG, browser support on older devices is imperfect, and the same numeric quality value produces very different visual results across formats. Each needs its own tuned settings.
The compound effect. A product listing page with 30 thumbnails. Without optimization: 30 × 200KB JPEG at 2048px = **6MB**. With the right width and format: 30 × ~25KB WebP at 360px = **750KB**. An 8x reduction – the difference between a page that loads in under a second on 4G and one that takes 15+ seconds on a slow connection. Multiply by millions of daily page views, and the infrastructure cost savings alone justify the system. But the real win is the user in a small town who can now browse products without watching a loading spinner.
What production distribution actually looks like. The theory above plays out clearly in practice. The format distribution across served images shows where the pipeline's format selection logic lands in aggregate - the balance between broad compatibility and bandwidth savings:

And the size distribution tells the other half of the story. With the right width normalization and format selection, the vast majority of served images land well under 100KB - a far cry from the multi-megabyte originals that sellers upload:

The question is how to manage this matrix – widths, formats, quality levels, crop strategies across dozens of teams without it becoming chaos.
Templates: The Heart of the Image Pipeline
Our answer was templates – named configurations that encapsulate the full set of transformation parameters for a given use case, living in config rather than scattered across client codebases:

Product catalog, marketing banners, seller onboarding different teams, different image requirements, and each combination growing more specific over time. Instead of if-else branches in handler code or magic numbers in client URLs, each use case gets a named configuration. Change the quality for all product thumbnails? Update one config entry. Roll out AVIF for a new surface? Add a template, zero code deploys.
Templates control not just what transformation happens but how the system behaves around it: whether the result is cached to storage (saveTransformed), what HTTP headers accompany the response, which storage bucket holds the output, and what quality and encoding settings are appropriate for a given format.
What makes this design powerful is that it's extensible. A template is a bag of properties and new properties extend what the pipeline can do without restructuring it. Take background removal as a concrete example. Adding removeBackground: true to a template definition is all it takes to activate that step in the pipeline for that use case — no new API endpoint, no handler changes, no client deploys. The template is the only thing that changes.
Deterministic Paths: The Decision That Makes Everything Else Work
The single most important design decision in Pixius is how it generates paths for transformed images. If the path for a given image + transformation combination is always the same, there's no need for a database to look up "where did we put the 800px WebP version of this product photo?" The URL is the lookup key.
Every transformation produces a fully deterministic path:

For example, an 800x600 JPEG at quality 85 with center crop becomes:

Notice the '__pixtr__' separator. That's not cosmetic, it's a sequence that will never appear in a real filename. This is the exact collision problem we hit with the old _720 suffixes, solved by making the separator impossible to occur naturally. Is sneaker_w800.jpg an original named "sneaker_w800" or an 800px-wide transform of "sneaker"? With '__pixtr__', there's no ambiguity, everything before it is the original filename, everything after it is transformation metadata. It also makes operational tasks trivial – grep for '__pixtr__' to find all transformed images, or exclude it to find only originals.
The CDN caches by URL. The URL maps 1:1 to a blob storage path. In production, this gives us a ~99% CDN cache hit rate – ~600K requests per second never even reach our servers. When the CDN cache does expire, the proxy checks blob storage with a simple object lookup – no database, no metadata query. Does this path exist? If yes, stream it. If not, redirect to the transformer.
There's a subtlety that makes deterministic paths work even better: width normalization. When a client requests a 350px wide image, Pixius doesn't produce a 350px variant – it snaps to the nearest canonical width (e.g., 360px). Without this, the result is a near-infinite key space: 349px, 350px, 351px, each generating its own cached variant that's visually indistinguishable from the others. Normalization collapses that space so requests from different clients all converge on the same cached path.
One more structural decision completes the picture: originals and transformed images live in separate buckets. Transforms are derived – they can always be regenerated. Keeping them apart means we can apply aggressive lifecycle policies on transforms (delete after 30 days of no access, downgrade storage class) while treating originals as durable source-of-truth. And if we ever need to nuke the transform cache after a quality algorithm change, say we do it knowing we're only deleting disposable artifacts, not originals.
How Requests Flow: Template Selection and Parameter Resolution
The answer is a two-step process – first resolve the template, then let query parameters override it:
Step 1: Template resolution. If the request includes a '?template=product-thumbnail' (or ?t=2 by ID) parameter, that template is selected. If not, the default template applies. Every deployment has a mandatory default template – typically a minimal baseline that sets saveTransformed: true and little else, ensuring every transformation result is at least cached.
Step 2: Query parameter overrides. Once the template is resolved, any additional query parameters – '?w=512&q=90&format=webp' – override the corresponding template values. The template provides the baseline; query params are the escape hatch. A team experimenting with a new width or a one-off marketing asset that doesn't fit any template can override without needing a config change.
These layers merge, not replace. If the default template sets 'saveTransformed: true and quality: 80', and the query string adds 'width: 512 and format: webp', the final transformation uses all of them: quality 80 from the template, width 512 and WebP format from the query, and the result gets saved. Each layer contributes what it specifies and defers to higher-priority layers for conflicts.
Filename-Embedded Parameters: Regeneration Without a Database
There's one more source of transformation parameters, and it falls out of deterministic paths naturally. Since the '__pixtr__' contract encodes a complete record of the transformation in the filename itself, Pixius can use it for regeneration.
When a transformed image is deleted – whether by a lifecycle policy or a storage cleanup and a request arrives for 'sneaker__pixtr__w800_q85_ccenter.jpg', Pixius doesn't return a 404. It splits on '__pixtr__', recovers the source filename (sneaker), extracts the transformation parameters (w800, q85, ccenter), fetches the original, and regenerates the variant. No database lookup, no metadata store, the filename itself carries everything needed for reconstruction.
If the request also includes query parameters – say '?w=512' – those override the filename-embedded values, but critically, the transformation is always applied to the original source image, not to the already-transformed variant. This is a deliberate quality decision: re-transforming an already-compressed image compounds quality loss. A JPEG decoded and re-encoded at a different width loses detail each generation. By always going back to the source, every variant is exactly one transformation away from the original, regardless of how many times it's regenerated or how many parameter overrides are applied.
The Architecture: Three Characters, One Binary

A design choice worth calling out: Pixius was built to be a generic image service that happens to run at Meesho – not a Meesho service that happens to handle images.
It ships as a single Go binary. What changes between deployments isn't the code, it's the configuration. A monolithic deployment would mean sizing every instance for the transformer's worst-case memory profile, even though the vast majority of requests are simple cache lookups. So instead: three deployables, three personalities, one codebase:
- Pixius (Proxy): The front door. Receives image requests, checks if a cached variant already exists in blob storage, and streams it back. No transformation, no uploads. Pure lookup-and-serve – almost entirely I/O-bound, pushing ~6K RPS at roughly 1000 RPS per core.
- Pixius-Transformer: The artist. When the proxy can't find a pre-transformed variant, it redirects (via HTTP 307) to the transformer. This instance fetches the original image from blob storage, applies transformations (resize, format conversion, quality adjustment, cropping) using libvips, caches the result with a deterministic path, and serves it. This is where real CPU work happens – decoding, encoding but only once per unique combination, handling ~660 RPS at roughly 300 RPS per core.
The latency profiles of these two deployables tell the story of why they're separated. The proxy's P99 stays tight - it's doing a GCS lookup and streaming bytes. The transformer's P99 carries the weight of decode-transform-encode cycles, with a heavier tail driven by large images and expensive codecs like AVIF:

- Pixius-Uploader: The intake clerk. Handles the write path – multipart form uploads, URL-based ingestion, optional on-upload transformation, and concurrent bulk processing. Upload routes are conditionally registered – they literally don't exist on the proxy and transformer deployments.
The selection happens through two environment variables: ENVIRONMENT (stg, prd) and PROFILE (transformer, uploader, or empty for proxy). These construct the config file path: 'application-prd-transformer.yml, application-stg-uploader.yml', etc. Same binary, different soul.
This config-driven design extends to infrastructure too. Every storage operation flows through a Storage interface – upload, download, stream, check existence, list, sign URLs. The handlers never import a cloud SDK directly. Today, Google Cloud Storage is the production backend, but the storage manager already switches on a storageBackend config key, with slots for S3 and local filesystem. Swapping cloud providers means implementing the same interface and changing one config value – zero handler code changes. Together with templates, these two axes – where we store and how we transform – mean Pixius can be dropped into completely different infrastructure and work out of the box.
And because the service is completely stateless, no local disk cache, no in-memory state, no sticky sessions – scaling is just adding pods. Need to handle a flash sale with 3x traffic? Bump the replica count. A fresh pod is ready to serve the moment it passes its health check. The compute layer is disposable; all state lives in blob storage.
Stream vs. Buffer: Why Separation Pays Off

The proxy and transformer have fundamentally different relationships with memory.
The proxy never loads image data into memory. When it finds a cached image in blob storage, it gets an io.ReadCloser – a streaming handle piped directly into the HTTP response writer via io.Copy(w, reader). Bytes flow from blob storage straight to the client. The proxy process never holds the image – it's just a conduit. For a 5MB product photo, the proxy's heap contribution is effectively zero.
The transformer has no choice but to buffer. libvips needs the complete image in a contiguous byte slice before it can decode, resize, or re-encode. At peak, a single transformation can hold two full images in memory – the source and the output. The separation lets each be right-sized – the proxy gets more cores and less RAM, the transformer gets fewer cores but enough headroom to buffer without OOM risk.
Semaphores: Controlled Degradation Under Load

Why are explicit concurrency limits needed in a system that's already horizontally scaled? The failure mode of a service dealing with large files isn't gradual slowdown — it's a cliff. A single transformation can consume 50-100MB of memory. Without limits, a traffic spike turns into a memory spike turns into an OOM kill turns into cascading restarts.
Pixius gates concurrency with three independent semaphore pools – upload, transformation, and streaming, each implemented as a buffered Go channel, the simplest possible semaphore. The streaming semaphore deserves special attention: even though the proxy doesn't buffer image bytes, each active blob storage stream occupies ~4MB in Go's HTTP/2 receive pipe. The semaphore caps how many streams can be open simultaneously, making the proxy's memory ceiling predictable: maxStreamingConcurrency x 4MB. Without it, the proxy, despite never "loading" an image, could still OOM from pipe buffer accumulation under a traffic spike.
The semaphores are context-aware too. If the request context expires while waiting for a slot, the request fails fast instead of queueing behind a backlog that will all timeout anyway. We'd rather return a clear error than accept a request we can't fulfill.
The Zombie Transform: Not Wasting Work Already Done
Here's a question: if a client disconnects while the transformer is mid-operation, should the transformer abandon the work? We call these zombie transforms – requests whose client is gone but whose work is still running and our answer is deliberate.
The naive answer is yes – the client is gone, why bother? But consider the economics. libvips operations are C code behind CGO – they can't be cancelled mid-execution. If you've already decoded a 4MB JPEG and are halfway through encoding an AVIF, the CPU cycles are spent regardless. And the next client asking for the same variant will trigger the exact same expensive transformation from scratch.
Pixius deliberately lets zombie transformations finish:
- The goroutine acquires the transformation semaphore
- It calls libvips for the actual image processing
- If the request context times out, the client gets a 504 Gateway Timeout
- But the goroutine keeps going, it finishes the transformation and uploads the result to blob storage
The client sees a timeout – a retryable one. Not a 500 ("something broke, good luck") but a 504 ("the work is taking longer than we promised, try again"). And that retry is where the magic happens: by the time the client asks again, the zombie goroutine has almost certainly finished and cached the result. The first request paid the cost, the second reaps the reward. The 504 wasn't a failure, it was a "come back in a moment."
The upload itself gets its own background context with a fresh timeout, so even if the original request is long dead, the blob storage write completes. The transformation semaphore stays held throughout, the system is honest about its capacity, but it doesn't throw away completed work.
The Uploader: One Way In
Remember the fragmented upload landscape from before Pixius – teams uploading through the blob storage console, client libraries, generic file services, nobody knowing what lived where? The uploader exists to replace all of that with a single, controlled entry point.
It handles the write path – authentication, validation, bulk processing, optional on-ingest transformation through one service, so every image that enters the system goes through the same checks and lands in the right bucket with the right metadata. No more mystery files appearing in storage with no audit trail.
Single uploads accept either a multipart file or a remote URL. When transformation is needed, the full body is buffered and validated (magic byte checks to ensure it's actually an image). When it's not, the upload streams directly to storage – no buffering, no intermediate copy.
Bulk uploads process items concurrently (max count is config-driven), each in its own goroutine with independent semaphore acquisition. The response includes per-item status, which succeeded, which failed, and why so callers can retry partial failures without re-uploading everything.
Upload routes are only registered when explicitly enabled in config – on the proxy and transformer deployables, these routes don't exist in the router. Not disabled. Not returning 403. Not present.
What We'd Tell You Over Coffee
The system design above has a lot of moving parts – but the decisions that actually mattered were surprisingly few. Here's what we'd distill if you asked us over coffee:
- Make your cache keys deterministic and derivable from the URL. No database lookups on the hot path. If the URL maps to a storage path, your origin server can be stateless.
- Separate your read path from your write path from your transform path. They have different resource profiles. Don't make your proxy pay for the transformer's CPU budget.
- Gate everything with semaphores. Not just for protection – for predictability. You'd rather reject requests cleanly than accept them and OOM.
- Let background work finish. If you've already spent the CPU cycles, don't throw the result away just because the client disconnected.
- Externalize transformation logic into templates. Opinions about image quality belong in config, not in client code scattered across dozens of microservices.
Pixius serves billions of images daily. Whether you're on fiber or 3G, you get the image that's right for your connection – the right size, the right format, the right quality. That's what democratizing internet commerce looks like at the pixel level.
Pixius is named after the mythical creature known for accumulating wealth – in this case, it hoards cached image variants so nobody has to generate them twice. That it also shares its first three letters with "pixel" is just the universe having a sense of humor.

