source.unsplash.com Is Gone: The Post-Mortem and Every Way to Replace It

Deprecated in 2021 with “existing uses will continue to work”, switched off in June 2024, and still being written into new code today. The measured post-mortem — and the three replacements, with the proxy that gives keyless randomness back.

A black-and-white photograph of a smartphone with a cracked screen lying on a pale wooden surface, the Unsplash logo showing on the damaged display.
Photo via Unsplash

A stock photo service turned off a subdomain, and two years later it is still breaking documentation sites, login screens, course exercises and freshly generated code. This is a post-mortem of a URL — what it did, what killed it, and what to put in its place — plus a measured look at the stranger part of the story: the machines that write our code have not noticed it is gone.

The HTTP responses, the two changelog entries quoted word for word, the API limits, the issue trackers of the projects that broke and the counts from GitHub, npm and Stack Overflow all come from primary sources, with the method for each one in the footnotes. If you only want the fix, jump to the migration table.

What you get today, if you still request it

One command, no key, reproducible from any machine:

terminal
curl -I https://source.unsplash.com/random

HTTP/2 503
cache-control: no-cache, no-store
content-type: text/html; charset=utf-8
server: Heroku
via: 2.0 heroku-router
# body: an iframe pointing at herokucdn.com/error-pages/application-error.html

Nothing here is a DNS failure. source.unsplash.com still resolves — it is a CNAME to a herokudns.com host — so the request is answered, just not by an application. That detail matters more than it sounds: a browser that gets a fast 503 with an HTML body renders a broken image placeholder, and any code that reads response.ok or an onerror handler you never wrote takes the failure path you never tested.

URL pattern What it used to return Today
source.unsplash.com/randomA random photo, any size503
source.unsplash.com/random/1600x900A random photo, cropped to size503
source.unsplash.com/1600x900/?apple,deskA random photo matching search terms503
source.unsplash.com/featured/1600x900?natureA random featured photo503
source.unsplash.com/collection/190727/800x600A random photo from a collection503
source.unsplash.com/user/scottwebb/1600x900A random photo from one photographer503
source.unsplash.com/dailyThe photo of the day503

Checked individually with curl -o /dev/null -w "%{http_code}". The search feature was wound down first, as announced; today the whole application is off, so the distinction no longer exists.

Three years between “deprecated” and “off”

Both announcements are still readable, in one place, at unsplash.com/documentation/changelog. Quoted in full, because the wording is the whole story:

25 November 2021 — “Unsplash Source being deprecated”
“Unsplash Source is being deprecated. Existing uses will continue to work, however for new projects use the full Unsplash API.”

11 June 2024 — “Unsplash Source sunset”
“Unsplash Source has been officially unsupported since its deprecation in 2021. As part of the final sunsetting, we will first wind down by disabling the search feature, and in the coming weeks turn off the application entirely. Existing uses of Source — particularly production-level ones — should migrate as soon as possible to the full Unsplash API.”

Read them in sequence and the failure mode is obvious. The 2021 notice contained a promise (existing uses will continue to work) and no date. A developer who read it in 2021 had every reason to leave working code alone; a developer who joined in 2022 never read it at all. The 2024 notice gave “the coming weeks”, three years later, on a page nobody had bookmarked.

Unsplash does publish a deprecation policy, and it is a reasonable one — the documentation states that for publicly documented fields and endpoints, changes are announced on the changelog with at least 3 weeks of notice, and endpoints return a Warning header during the deprecation period. The same paragraph contains the sentence that explains why none of it protected Source: “For any non-publicly documented fields or endpoints, we may make changes to these with no warning.” Source was never an endpoint of the documented API. It sat outside the policy that would have covered it.

  • The practical lesson is not “Unsplash was careless”. It is that a URL you can use without reading any documentation is a URL whose deprecation policy you have also not read.
  • Breakage preceded the announcement. A Drupal issue filed on 28 November 2022 already reports “I get always a Heroku application error”, eighteen months before the sunset entry. Intermittent failure is how these services die: slowly, then in an announcement you never see.

The announcement is harder to find than the outage

The 2021 deprecation was published on changelog.unsplash.com, and that is the URL every contemporaneous bug report links to — including the Drupal one above. Three measurements:

  1. The HTTPS endpoint is broken. openssl s_client -connect changelog.unsplash.com:443 returns tlsv1 alert internal error — the handshake fails before any certificate is presented. Every 2021-era link, which was https://, is therefore dead in a browser.
  2. Over plain HTTP it redirects, but not usefully. Following http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.html ends, two hops later, on a 400 at unsplash.com/@documentation/changelog/deprecations/2021/11/25/source-deprecation/html — the path has been swallowed by the site's username route.
  3. The archive has a hole where the sunset is. The Wayback Machine's last successful capture of the old changelog is 24 March 2024; its first capture of the new one is 23 August 2024. The sunset was announced on 11 June 2024 — inside that five-month gap.

None of that is a conspiracy; it is an ordinary CMS migration. But the consequence is real, and it is the reason this article quotes both entries in full: the primary record of a deprecation should outlive the thing it deprecated, and here it very nearly did not.

What actually broke

Not side projects. The failures below are public issue-tracker entries; the titles, dates and states come from the GitHub and drupal.org APIs.

Project Issue Opened What it says
MUI (Material UI) #42736 24 Jun 2024 “[docs] Random Unsplash photo URL is no longer functional” — the official Sign-in side template shipped a dead image. Closed three days later.
Nextcloud #115 17 Jan 2023 “Migrate to Unsplash API” — the background app was built on Source URIs. Open for eighteen months, closed 16 July 2024.
sindresorhus/Actions #248 28 May 2024 “Get Unsplash Image: 503 Error” — an iOS/macOS Shortcuts action, broken two weeks before the sunset was announced.
Drupal — Gin Login #3324054 28 Nov 2022 “Unsplash has deprecated source.unsplash.com — this delays reCAPTCHA from loading, preventing users from logging in.”

Read that last row again, because it is the one worth internalising. A decorative image next to a login form — the most obviously non-critical asset on the page — degraded into an authentication outage, because a slow third-party request sat in front of the CAPTCHA the login form needed. Nobody wrote it that way. It emerged from the order in which a browser loads things.

Your coding assistant did not get the memo

Here is the part that turns a 2024 outage into a 2026 problem. source.unsplash.com was documented, blogged, taught and copied for roughly eight years. All of that text is in the training data of the models that now write our starter code — and text does not expire. Three counts:

MeasurementValue on 30 Aug 2026How it was taken
Files containing source.unsplash.com 3,344 GitHub code search API, q=source.unsplash.com (indexed public code only — a floor, not a total)
Repositories in a 100-file sample created after the sunset 12 of 77 Same query, 100 results, deduplicated to 77 repositories, created_at compared to 11 Jun 2024
…and repositories in that sample pushed to in the last 12 months 20 of 77 Live repositories, not archives — including elastic/kibana, whose demo file still reads imageUrl: 'https://source.unsplash.com/64x64/?dingo'
Monthly downloads of unsplash-source-es6 23 npm registry API — a wrapper for a dead service, last published in 2022, still being installed
Stack Overflow posts mentioning it 1,459 Stack Exchange API, /search/excerpts total

The most direct evidence is not in application code at all — it is in prompts. The top result for that search is a library of GPT system prompts containing the line “please use unsplash API( https://source.unsplash.com/1280x720/?<PUT YOUR QUERY HERE>”. That instruction is still being copied into new assistants today. The model does not verify the URL; it was told to use it, and every example it ever saw agreed.

So the generated-code failure has two independent causes, and fixing one does not fix the other: stale training data, and stale instructions written by humans on top of it. Either way, the symptom is the same family of images that never load:

the dead-image patterns worth grepping for
source.unsplash.com/random/1200x800   # 503 since mid-2024 — never comes back
images.unsplash.com/photo-…           # real CDN, but memorised IDs may not exist
via.placeholder.com/400               # grey rectangle, shipped to production
placehold.co/800x600                  # grey rectangle, on purpose
picsum.photos/800/600                 # a real photo, unrelated to your page
/placeholder.png                      # a file that was never added to the repo

A footnote on the second line of that list: while writing this, via.placeholder.com would not complete a TLS handshake from our test network either, and answered 403 over plain HTTP. Check it from your own network before trusting it — the fallback these tools reach for may have an outage story of its own.

Only the first one is broken. The others are worse in a subtler way: they load, the layout looks finished, and nobody notices that the page is illustrated with nothing in particular. And none of this is specific to images — it is the general shape of the problem. A model's picture of the web is a snapshot, and endpoints, CLI flags, package names and free tiers keep moving after the shutter closes.

The migration table

There are exactly three destinations, and the honest way to present them is by what you give up. Pick the column first, then read your row.

Old Source URL A. Fixed CDN URLno key · no randomness B. Unsplash APIkey · server-side call C. Your own proxykey hidden · randomness back
/random images.unsplash.com/photo-… — one photo you chose GET /photos/random /?w=1600
/random/1600x900 …?w=1600&h=900&fit=crop /photos/random + Imgix params on the returned URL /?w=1600&h=900&fit=crop
/1600x900/?apple,desk No equivalent — pick a photo by hand /photos/random?query=apple,desk /?query=apple,desk&w=1600
/featured/1600x900?nature No equivalent /photos/random?query=nature “featured” has no successor /?query=nature&w=1600
/collection/67920491/1600x900 No equivalent /photos/random?collections=67920491 /?collections=67920491&w=1600
/user/scottwebb/1600x900 No equivalent /photos/random?username=scottwebb /?username=scottwebb&w=1600
/daily Pin one photo, rotate it in your build No equivalent — cache one random photo for 24 h yourself Same, with the cache in the proxy

Option A is the one most people actually want. If the image was decorative — a hero, a login side panel, a card background — you never needed a different photo on every request. Choose one, keep the CDN URL, and the page stops depending on anything random:

a fixed, resizable Unsplash URL — no key, no API call
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&h=900&fit=crop&auto=format"
     width="1600" height="900" alt="…">
# Officially supported params: w, h, crop, fit, fm, auto=format, q, dpr.
# Keep any ixid parameter the API gave you — it is what reports the view.

Option B is the official path, and it moves the call server-side, because a Client-ID in front-end JavaScript is a published credential. Note the two rules that trip people up: collections/topics cannot be combined with query in the same request, and count (max 30) changes the response shape to an array even when it is 1.

the official replacement for /random
curl "https://api.unsplash.com/photos/random?query=nature&orientation=landscape" \
  -H "Authorization: Client-ID YOUR_ACCESS_KEY" \
  -H "Accept-Version: v1"

# → JSON. The image lives at .urls.regular / .urls.raw (add w/h/fit yourself).
# → X-Ratelimit-Limit: 1000   X-Ratelimit-Remaining: 999

Option C: rebuild Source, in about forty lines

If what you lost was genuinely the behaviour — a keyless URL that returns a different photo each time, usable straight from an <img> tag, in a CMS field, or in a static site where there is no server — then you have to run that endpoint yourself. It is one small worker in front of the API, and the three things that make it survive contact with production are the cache, the referrer check, and passing the rest of the query string through to the CDN.

worker.js — a keyless Source-shaped endpoint on top of /photos/random
// Cloudflare Workers. Elsewhere (Deno Deploy, Val Town…) the shape is the same,
// but open a named cache with caches.open() instead of caches.default.
// UNSPLASH_KEY stays server-side. Callers never see it.
const ALLOWED = ["example.com", "www.example.com"];   // your domains only
const API_PARAMS = ["query", "collections", "topics", "username", "orientation"];
const TTL = 60;                                       // seconds — protects the hourly quota

const host = (value) => { try { return new URL(value).hostname; } catch { return null; } };

export default {
  async fetch(req, env, ctx) {
    // 0. GET only: the Cache API refuses to store anything else, and an image
    //    endpoint has no other verb to answer.
    if (req.method !== "GET")
      return new Response("Method not allowed", { status: 405 });
    const url = new URL(req.url);

    // 1. Only your own pages may embed it — a public random-photo endpoint
    //    on the open internet is someone else's rate limit to burn.
    const ref = req.headers.get("referer");       // absent on plenty of legit clients
    if (ref && !ALLOWED.includes(host(ref)))
      return new Response("Forbidden", { status: 403 });

    // 2. Cache per parameter combination, so a page with 12 images
    //    costs one API call per minute instead of twelve per render.
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    // 3. Ask the official API for a random photo.
    const api = new URL("https://api.unsplash.com/photos/random");
    for (const p of API_PARAMS)
      if (url.searchParams.has(p)) api.searchParams.set(p, url.searchParams.get(p));

    const r = await fetch(api, { headers: {
      Authorization: "Client-ID " + env.UNSPLASH_KEY,
      "Accept-Version": "v1",
    }});
    // 403 here usually means the hourly quota, not a bad key — raise TTL, not panic.
    if (!r.ok) return new Response("Upstream " + r.status, { status: 502 });
    const photo = await r.json();

    // 4. Rebuild the image URL: keep ixid, append the caller's sizing params.
    const img = new URL(photo.urls.raw);          // .raw already carries ixid
    for (const [k, v] of url.searchParams)
      if (!API_PARAMS.includes(k)) img.searchParams.set(k, v);  // w, h, fit, q…

    const res = new Response(null, { status: 302, headers: {
      Location: img.toString(),
      "Cache-Control": "public, max-age=" + TTL,
      // Credit travels with the redirect; header values must be ASCII, hence the encoding.
      "X-Photo-Credit": encodeURIComponent(photo.user.name + " on Unsplash"),
      "X-Photo-Link": photo.links.html,
    }});
    ctx.waitUntil(cache.put(req, res.clone()));
    return res;
  },
};

Migrating a URL is then a search-and-replace, which is exactly what makes this option worth the twenty minutes:

one-for-one replacement
- https://source.unsplash.com/collection/67920491/1600x900
+ https://img.example.com/?collections=67920491&w=1600&h=900&fit=crop

Two design notes, both of which cost someone a bad afternoon before they were written down. The redirect (302) rather than proxying the bytes keeps you off the hook for bandwidth and keeps the view counted on Unsplash's CDN, which is what the guidelines ask for. And the Referer check is deliberately permissive when the header is absent — plenty of legitimate clients strip it — while still stopping the obvious case where your endpoint becomes somebody else's free image API.

The rules you inherit the moment you use the API

Source had no rules because it had no account. The API has five that change how you architect the thing, all from the current documentation:

  • Rate limits are per hour, and small at first. 50 requests/hour in demo mode; 1,000/hour after your application is approved for production. Only calls to api.unsplash.com count — image requests to images.unsplash.com do not. Read X-Ratelimit-Remaining on every response.
  • Hotlinking is mandatory, not merely allowed. Unsplash requires that the image URLs the API returns be embedded directly, so photo views can be attributed to the photographer. Mirroring the file onto your own CDN is the one optimisation you are not free to make.
  • Keep the ixid parameter. Resizing and cropping the returned URL is expected; stripping the parameter that identifies your application is not.
  • Attribution and download tracking are part of the deal — the photographer and Unsplash get credited, and a “download” is reported through the photo's download endpoint when a user takes the file, which is an event you have to fire yourself.
  • Distributed products need dynamic client registration. If you ship a plugin, a theme or a self-hosted CMS, one shared key is both a policy violation and a single point of failure; the API has a registration flow for exactly that case.

This is the moment to be honest about scope: if you are wiring up an API and a key anyway, the choice of which image API is suddenly open, and it is worth spending five minutes on before you write the client. We compared the free ones — quotas, rules, search behaviour and response shapes — in the free stock photo API comparison.

If you only wanted a placeholder, say so

A large share of Source usage was never about Unsplash. It was “put something image-shaped here while I build the layout”. For that, keyless services still exist and are the correct answer:

ServiceKey?What you getWhere it stops
Lorem Picsum No Real photographs: picsum.photos/800/600, a stable one with /id/237/… or /seed/xxx/…, plus ?grayscale and ?blur=1..10. Its /v2/list endpoint credits each photo's Unsplash page and author. No subject targeting at all. The photo will not relate to your page.
placehold.co No Labelled rectangles at any size — honest wireframe filler. It is a grey box, and it looks like one in a screenshot shared with a client.
Openverse No An openly licensed catalogue with a public API, run by WordPress.org. Keyword matching, and licences vary per item — you must read them.

The distinction that matters: a placeholder is temporary by definition. If the image survives to production, it is not a placeholder any more — it is an illustration that nobody chose, and the reader can tell.

One key instead of three

Here is the part of the migration nobody plans for: people leaving Source rarely land on one API. A page needs a hero, two section images and something for a card grid, and the honest answer is usually Unsplash plus Pexels plus Pixabay — three registrations, three auth schemes, three JSON shapes, three pagination models and three sets of attribution rules, all to fill the same <img> tags. That integration work is the real bill for a keyless URL going away, and it lands weeks after the outage that caused it.

Collapsing that into one integration is what we built Pexafy for: a single key over 9 free-licence libraries in one schema, with sentence-level semantic search — so a full description like “a cracked phone screen on a wooden desk, shot from above” returns ranked results instead of nothing. Two limits, stated plainly, because this whole article is about not being surprised twice: it needs a key, so it does not restore what Source was — it belongs in the same category as the official Unsplash API above; and it carries free-licence photography, not editorial or brand imagery.

The piece that is genuinely new is aimed at the assistants above: an MCP server at mcp.pexafy.com/mcp means a model that would otherwise recite an image URL from memory can search a real catalogue and return a photo that exists, with its credit line attached. That is a better answer to machine-written dead URLs than any lint rule, and the reasoning is written up in image search infrastructure for AI agents.

Audit your estate in ten minutes

Whatever you migrate to, do this part first — you cannot fix URLs you have not found. Source is just today's example; the same three steps apply to every external asset you embed.

find every dead reference, then keep them out
# 1. Everything in the repo, including docs, tests, fixtures and READMEs.
grep -rn --binary-files=without-match \
  -e "source.unsplash.com" -e "via.placeholder.com" -e "/placeholder.png" .

# 2. Everything the database holds — CMS bodies are where these hide longest.
psql -c "SELECT id FROM posts WHERE body LIKE '%source.unsplash.com%'"

# 3. Everything the built site actually requests: crawl it and list the failures.
#    Match the attribute, not a file extension — image URLs rarely end in .jpg.
grep -rhoE 'src="[^"]+"' dist/ \
  | cut -d'"' -f2 | grep -E '^https?://' | sort -u \
  | xargs -P8 -I{} curl -s -o /dev/null -w "%{http_code} {}\n" {} \
  | grep -v "^200"

# 503 https://source.unsplash.com/random/1200x800   ← what you are looking for

Then decide, once, how much external images are allowed to cost you. Four rules that survive the next shutdown, whoever causes it:

  1. Fetch at build time, not at request time. An image resolved during the build fails in CI, in front of a developer, instead of at 3 a.m. in front of a user.
  2. Never let a decorative asset block a critical path. Preload nothing external above a login form; give every third-party <img> an onerror fallback and explicit width/height so a failure costs a blank box, not a layout shift or a stalled script.
  3. Add the check to CI. Step 3 above, run on your built output, turns “someone eventually noticed” into a red build. It is the only step that prevents recurrence.
  4. Budget your external dependencies like any other. Write down which hosts your pages are allowed to depend on and what happens when each one is down. A URL you did not have to register for is still a dependency — Source proved that it is simply one nobody owns.

References & footnotes

1 Every status code, count and quoted line in this article was taken from its primary source on 30 August 2026. HTTP status codes were taken with curl against each URL pattern; all returned 503 with server: Heroku and a body embedding herokucdn.com/error-pages/application-error.html. DNS resolution was confirmed the same day (a CNAME to a herokudns.com host).

2 Both changelog entries are quoted verbatim from unsplash.com/documentation/changelog. The deprecation-policy wording (3 weeks of notice, Warning header, and the exemption for non-publicly documented endpoints) comes from unsplash.com/documentation, same day.

3 TLS failure reproduced with openssl s_client -connect changelog.unsplash.com:443 (tlsv1 alert internal error). Redirect chain followed with curl -L. Archive gap taken from the Wayback CDX API: last 200 capture of changelog.unsplash.com on 20240324, first of unsplash.com/documentation/changelog on 20240823.

4 Issue titles, creation and closing dates read from the GitHub REST API (mui/material-ui#42736, nextcloud/unsplash#115, sindresorhus/Actions#248) and from the drupal.org JSON API for gin_login issue 3324054, whose body reports “I get always a Heroku application error” in November 2022.

5 Counts: GitHub code search API (3,344 files; a 100-result sample deduplicated to 77 repositories, of which 12 were created after 11 June 2024 and 20 had been pushed to within the previous 12 months); npm registry downloads API (unsplash-source-es6, 23 downloads over the preceding 30 days); Stack Exchange /search/excerpts (1,459 posts). Code search covers indexed public repositories only, so each figure is a floor.

Frequently asked questions

Is source.unsplash.com down, or has it been shut down permanently?
Permanently. Unsplash announced the sunset on 11 June 2024 — “we will first wind down by disabling the search feature, and in the coming weeks turn off the application entirely” — after deprecating the service on 25 November 2021. Every pattern (/random, /1600x900/?query, /collection/…, /daily) returns HTTP 503 with Heroku’s generic Application Error page. The hostname still resolves, so the failure shows up as a broken image rather than a network error.
What is the direct replacement for source.unsplash.com/random?
There is no keyless drop-in, and that is the part worth accepting early. Three replacements exist. A fixed CDN URL — images.unsplash.com/photo-…?w=1600&h=900&fit=crop — needs no key but always returns the same photo, which is what most decorative uses actually needed. The official API, GET https://api.unsplash.com/photos/random with an Authorization: Client-ID header, restores randomness but must be called server-side. A small proxy you own in front of that endpoint is the only option that gives back a keyless URL you can drop straight into an <img> tag.
Why do AI coding tools still generate source.unsplash.com URLs in 2026?
Because the URL was documented, taught and copied for roughly eight years, and a training corpus does not expire when a service does. Measured on 30 August 2026: GitHub code search still returns 3,344 files containing it, and in a 100-file sample 12 of 77 repositories were created after the shutdown. Human-written prompt libraries repeat the instruction too — one widely copied GPT prompt still tells the model to “use unsplash API( https://source.unsplash.com/1280x720/?… )”. Treat any image URL a model writes as unverified and check status codes in CI.
Can I still get a random Unsplash photo without an API key?
Not from Unsplash directly — random selection now lives behind /photos/random, which requires a Client-ID. Your two keyless routes are a proxy you host yourself, where the key stays server-side and the public URL looks like the old one, or a third-party placeholder service such as Lorem Picsum (picsum.photos/800/600), which serves real photographs with no key but no subject targeting at all.
Does the Unsplash API allow downloading and self-hosting the images?
No. Unlike most APIs, Unsplash requires hotlinking: the image URLs the API returns must be embedded directly, so photo views are counted for the photographer. Three obligations come with it — keep the ixid parameter when you resize or crop the URL, credit the photographer and Unsplash, and fire the photo’s download endpoint when a user takes the file. Mirroring the files onto your own CDN is the one optimisation you are not free to make.
Why did the 2021 deprecation notice not protect existing users?
Because the notice and the policy did not cover the same thing. The changelog entry of 25 November 2021 promised that “existing uses will continue to work” and gave no end date. Unsplash’s published deprecation policy — at least three weeks of notice plus a Warning header — applies to publicly documented fields and endpoints, and the same paragraph states that anything undocumented may change with no warning. Source was never a documented endpoint of the API, so it sat outside the policy that would have protected it.
How do I find every dead source.unsplash.com URL in my project?
Three passes, ten minutes. Grep the repository including docs, tests, fixtures and READMEs — grep -rn "source.unsplash.com" . — since these URLs survive longest in sample code. Query the database, because CMS article bodies are where they hide (WHERE body LIKE '%source.unsplash.com%'). Then crawl your built output: extract every image URL and request each one, listing anything that is not 200. Add that last pass to CI and a dead third-party asset fails a build instead of a page.

Stop hunting for keywords. Describe what you mean.

Search 9M+ free-to-use images by meaning — in any language, in under 100 ms.