Back to Blog

How We Built a Free Minecraft Avatar API

5 min read

Building an API that serves millions of Minecraft avatar requests per month sounds straightforward on the surface: fetch a player's skin, crop it, return the image. In practice, the engineering challenges run much deeper. This is the story of how we built MC Heads, a free and open API for Minecraft player avatars, and the decisions that keep it fast, reliable, and free.

The Image Processing Pipeline

Every request to MC Heads triggers a pipeline that varies depending on the render type. For a simple head render, we extract the 8x8 pixel face region from the player's 64x64 skin texture, then scale it up to the requested size using nearest-neighbor interpolation. This preserves the crisp, blocky aesthetic that makes Minecraft art distinctive — no blurring, no anti-aliasing, just clean pixel art.

For avatar renders, the process gets more interesting. We need to composite multiple layers: the base head, the hat overlay (the outer layer of the skin), and optionally render them from an isometric perspective. The isometric rendering involves applying an affine transformation to the front, top, and side faces of the head, then compositing them together to create the illusion of a 3D cube. Getting the pixel alignment right at various output sizes was one of the trickier problems we solved early on.

Full body renders take this even further. We extract each body part from the skin texture — head, torso, arms, legs — apply the appropriate transformations, and composite them into a standing player figure. Each body part has its own overlay layer, and we need to handle both the classic (4px wide arms) and slim (3px wide arms) skin models correctly.

Handling Java and Bedrock Editions

One of our key differentiators is native support for both Java and Bedrock Edition players. These two editions use completely different identity systems. Java Edition players are identified by UUIDs obtained from the Mojang API, while Bedrock Edition players use Xbox Live gamertags resolved through the GeyserMC API.

When a request comes in, we first need to determine which edition the player belongs to. We use a simple convention: Bedrock player names are prefixed with a dot (e.g., .PlayerName). This lets us route the request to the correct upstream API without ambiguity.

The Mojang API flow looks like this: first we resolve the player name to a UUID via api.mojang.com/users/profiles/minecraft/{name}, then we fetch the profile with skin URL via sessionserver.mojang.com/session/minecraft/profile/{uuid}. The skin URL is base64-encoded inside a textures property, so we decode that, download the skin PNG, and feed it into our rendering pipeline.

For Bedrock players, GeyserMC provides a similar flow but with different endpoints and data formats. We abstract over both of these behind a unified internal interface so the rendering pipeline does not care which edition the skin came from.

Caching with SQLite

Hitting upstream APIs for every single request would be slow and would quickly get us rate-limited. Our caching layer is built on SQLite, which turned out to be a surprisingly good fit for this use case.

Each cache entry stores the player's UUID (or Xbox ID), the raw skin texture as a blob, and a timestamp. We use a one-hour TTL for cache entries, which strikes a balance between freshness (players do change their skins) and performance. SQLite's WAL mode gives us excellent concurrent read performance, which matters when we are serving hundreds of requests per second.

We also cache the rendered images themselves, keyed by player identifier, render type, size, and direction. This means the most popular players — Notch, Dream, Technoblade — get served directly from the render cache without any image processing at all. The cache hit rate for rendered images sits around 85% in production, which is the single biggest factor keeping our response times under 100ms.

One important detail: we use a separate cache cleanup job that runs every 15 minutes to evict expired entries. We chose not to use lazy expiration (checking TTL on read) because we wanted to keep the database file size bounded. SQLite does not automatically reclaim space from deleted rows, so we run VACUUM during off-peak hours.

Isometric Rendering

The isometric head render is probably the most technically interesting part of the system. To create the 3D appearance, we take three faces of the Minecraft head — front, top, and right side — and project them onto an isometric plane.

The math involves a skew transformation. The front face gets skewed by -30 degrees, the right face by +30 degrees, and the top face gets a more complex transformation involving both horizontal scaling and rotation. We perform all of these operations at the pixel level to maintain the crisp Minecraft aesthetic.

Getting the seams right between faces was a significant challenge. At small output sizes, rounding errors in the transformation can create 1-pixel gaps or overlaps between faces. We solved this by rendering at 2x the target resolution and then downscaling with nearest-neighbor, which ensures clean pixel boundaries.

Rate Limiting and Fair Use

Even though we advertise "no rate limits," we do have some protections in place. We use a token bucket algorithm per IP address to prevent abuse, with a generous limit of 300 requests per minute. In practice, legitimate users rarely exceed 30 requests per minute, so this only catches scrapers and misconfigured bots.

We also monitor for patterns that suggest someone is trying to enumerate all possible player names. These requests get automatically throttled to protect both our infrastructure and the upstream Mojang API.

CDN and Edge Caching

The final layer of our performance stack is CDN edge caching. We set aggressive Cache-Control headers on rendered images: public, max-age=3600, s-maxage=86400. This means CDN edge nodes cache renders for up to 24 hours, while browsers cache for 1 hour.

The CDN absorbs a huge percentage of our traffic. For the most popular players, the request never even reaches our origin server — it is served directly from an edge node within milliseconds. We estimate that CDN caching reduces our origin server load by about 70%.

We also use Vary: Accept headers to ensure that different image format negotiations do not interfere with caching, and we include ETag headers so that conditional requests can be satisfied with a 304 response, saving bandwidth.

What We Learned

Building MC Heads taught us that a well-designed caching strategy can make the difference between a service that costs hundreds of dollars per month and one that runs essentially for free. SQLite as a cache store is underrated — it is fast, reliable, requires zero configuration, and handles concurrent access well in WAL mode.

The other key lesson is that upstream API design matters enormously. Mojang's API has its quirks (base64-encoded skin URLs inside JSON inside base64), and building a robust pipeline that handles all the edge cases — name changes, missing skins, API timeouts — takes more engineering effort than the core image processing.

If you are building something similar, start with the caching layer. Get that right, and everything else becomes much easier.