Skip to content
All documentation pages

Caching, limits, fallbacks and errors

How Skin Render caches profiles, textures and PNGs, which response headers to rely on, how Mojang rate limits affect first requests, fallbacks and error codes.

Cache layers

A render passes through four in-memory caches on our side 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. The render itself is 20–25 ms for a 512 px body and around 100 ms at 1024 px; X-Render-Ms reports the exact figure.

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.
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.

If you are planning something big (a launcher, a server list with millions of daily views), an email to hello@skinrender.dev beforehand is appreciated so capacity can be planned.

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. GET /api/health tells you whether the service is up. The about page has the source code, so you can also run your own copy.