Skip to content
All documentation pages

Caching, limits, fallbacks and errors

How the skin cache works: profiles for 15 minutes, textures for a day, rendered PNGs for an hour. Plus the headers to rely on, Mojang rate limits and errors.

Cache layers

A render passes through four in-memory caches on our side (names, profiles, skin textures and finished PNGs) before it reaches yours. Each has its own lifetime, chosen to match how often the underlying thing actually changes.

WhatLifetimeWhy
Username → UUID1 hour (10 minutes for a name that does not exist)Names change rarely and Mojang rate-limits the lookup; a UUID in the URL skips this step entirely.
Profile (UUID → skin and cape URLs, model)15 minutes, then served stale for up to 6 hours while it refreshes in the backgroundSkin changes show up within a quarter of an hour; if Mojang is down the last known profile keeps working.
Texture (the PNG from textures.minecraft.net)24 hoursA texture hash is content-addressed: the same hash is always the same bytes, so this is safe for far longer.
Rendered PNG1 hourRepeated requests for the same URL are answered without rendering at all (X-Cache: HIT).

The practical effect: the first request for a player takes a few hundred milliseconds (a Mojang round trip plus a texture download plus the render), the next hour of requests for that URL take a couple of milliseconds, and any other URL for the same player still skips the Mojang part.

What a render costs

Rasterising and encoding are the only work a cache miss does once the texture is in memory, and X-Render-Ms reports exactly that. Measured on the production server, on requests forced to miss the render cache, with the process warm — the first renders after a restart are several times slower while the JIT catches up. The ranges are wide because they move with whatever else the machine is doing:

RequestX-Render-Ms
face?size=64, the common avatarabout 1 ms
head?size=128about 1 ms
body?size=2562–3 ms
body?size=5123–9 ms
body?size=1024, the largest allowed10–30 ms
body?size=1024&zoom=8&fov=120, about the most expensive request the parameters allow20–30 ms

Add your own network time to those, and on the very first request for a player the Mojang round trip above. A repeat of the same URL does no rendering at all and reports X-Render-Ms: 0 with X-Cache: HIT. If a figure here ever looks optimistic, check the header on your own request rather than believing this page — it is the same number, measured for you.

Response headers

HeaderValue
Content-Typeimage/png; errors are text/plain.
Cache-Controlpublic, max-age=3600, stale-while-revalidate=86400 for renders of a username or UUID. Renders of a texture hash or of steve/alex get public, max-age=31536000, immutable, because nothing about them can ever change. A render that fell back to a default skin because Mojang could not be reached gets public, max-age=60, so the real skin is retried soon; 400 and 404 errors are also public, max-age=60.
ETagSend it back as If-None-Match for a 304. Weak tags (W/"…") are matched too.
Access-Control-Allow-Origin*. Draw the image on a canvas or fetch it from any origin.
X-Render-MsMilliseconds spent rasterising and encoding the image, with one decimal; 0 when the PNG came from the cache.
X-CacheHIT or MISS for the rendered-PNG cache.
X-Skin-SourceWhat the identifier turned out to be: name, uuid, texture or default (for steve/alex).
X-Skin-FallbackPresent only when a default skin was rendered instead of the requested one, with the reason: not-found (no such player or UUID), no-skin (the profile exists but has no custom skin) or upstream-error (Mojang could not be reached and nothing was cached).
X-Ignored-ParamsComma-separated names of query parameters that were not recognised or do not apply to this render type, exactly as you wrote them. Use it to catch typos.
Content-Dispositionattachment; filename="<id>-<type>.png" when download=1 is set, or filename="<filename>.png" when you pass filename.
X-Robots-Tagnoindex on every /render/ and /api/ response, so search engines never index an image URL as a page.

The documentation pages themselves are cached the other way round: public, max-age=0, must-revalidate with an ETag over the HTML. A page is never served stale, and a revisit costs a 304 rather than the whole document. Images want a long max-age because they change when a player changes skin; a page wants a cheap check because it changes when we edit it.

bash
curl -sI "https://skinrender.dev/render/069a79f444e94726a5befca90e38aaf5/head?size=128&rotat=20"
# HTTP/2 200
# content-type: image/png
# cache-control: public, max-age=3600, stale-while-revalidate=86400
# etag: "362f78ce9157fa470e63f8cc0df"
# access-control-allow-origin: *
# x-render-ms: 2.2
# x-cache: MISS
# x-skin-source: uuid
# x-ignored-params: rotat
# x-robots-tag: noindex

Mojang rate limits

Mojang’s session server allows roughly one profile lookup per minute per UUID. Skin Render runs behind that limit on your behalf: it caches every profile for 15 minutes, so one Mojang call serves everyone who asks for that player in that window.

What this means for you:

  • A player’s skin change appears within about 15 minutes, not instantly: the first request after the profile expires still gets the old skin while a background refresh runs, and the next one gets the new. Nothing you pass can force a refresh, because Mojang would refuse the lookup anyway.
  • If Mojang is slow or down and the profile is not cached, the request falls back to Steve/Alex (see below) rather than hanging. With default=none you get a 502 instead.
  • Texture-hash identifiers and steve/alex never touch Mojang and are unaffected by any of this.

Fallbacks

The default parameter decides what happens when a player cannot be resolved or has no custom skin.

ValueBehaviour
auto (default)Render Steve or Alex, chosen from the UUID the way the game chooses a default skin (Steve when the identifier was a name that does not exist). The response is a normal 200 PNG with X-Skin-Fallback set to not-found, no-skin or upstream-error.
steve / alexAlways that skin as the fallback. fallback is an alias of default.
noneNo substitution: 404 for an unknown player or a profile without a custom skin, 502 when Mojang could not be reached.

Use auto in <img> tags, where a broken image is worse than a default skin, and none in code that wants to know the truth.

Error codes

Error bodies are plain text (text/plain), one line, meant to be read by a human in a log. They always name the parameter or identifier and quote what you sent.

StatusCauseExample body
400A parameter outside its range or of the wrong shape.Parameter "size" must be a whole number between 8 and 1024 (got "5000").
Parameter "crop" must be true or false (also 1/0, yes/no, on/off) (got "maybe").
400Unknown render type.Unknown render type "nope". Use one of face, head, bust, body, front, back, skin, cape (or the aliases avatar=face, full=body, portrait=bust).
400Bad identifier (too long, bad characters, malformed hash).Invalid identifier "a-b": expected a Minecraft username (1-16 letters, digits or underscores), a UUID (with or without dashes), a 64-hex texture hash (optionally prefixed "texture:"), or "steve" / "alex".
404Player not found or no custom skin, and default=none.No player named "zzzzqqqqxxxx9999".
No profile with UUID 00000000000000000000000000000001.
Notch has no custom skin.
502Mojang unreachable and nothing cached, and default=none.Mojang could not be reached to load profile 069a79f444e94726a5befca90e38aaf5.

Angles never error: values outside ±360 wrap. Unknown parameters never error either; they are ignored and reported in X-Ignored-Params. Sizes are the exception to leniency: anything outside 8–1024 px per side is a 400, never clamped.

Determinism

The renderer is a pure function of the texture and the parameters. The same URL for the same texture always produces byte-identical output, on every server, after every deploy that does not change the renderer. That is what makes the ETag honest and why you can cache renders for as long as you like: they only change when the player changes skin.

Fair use

There is no API key and no rate limit on our side, and there is no logging of who requested what beyond standard server logs (IP, path, status, timing) that rotate. In return, please:

  • Cache on your side too: honour Cache-Control, use the ETag, or just store the file. A CDN in front of your site does this for free.
  • Do not hot-loop the same URL; it is served from memory but the bandwidth is real.
  • Request the size you display. A 96 px avatar does not need a 1024 px render.
  • Prefer UUIDs to usernames for anything stored: they are stable and they skip a name lookup.

Uptime

Skin Render is free and run as a best-effort service by one person. There is no SLA, no status page yet and no guarantee beyond a genuine effort to keep it fast and available. Design for that: keep a cached copy of the images you depend on, set a timeout on your requests, and let default=auto paper over Mojang outages.