July 25, 2026
You searched "is perplexity down" because of the lawsuit headlines. It is not down, and it is not shutting down. Revenue is climbing. What the suits from CNN, the NYT, Dow Jones, Reddit and Amazon threaten is which sources the engine may read, so plan for a thinner citation set arriving inside a perfectly healthy 200 response.
The fear is reasonable. Named plaintiffs, an injunction, and a subreddit arguing about crawler ethics all landed in the same few months, and the obvious reading is a company about to fall over.
Revenue rules out exactly one thing: insolvency. The Financial Times reported annualized recurring revenue topping $450 million in March 2026, up 50% in a single month, with more than 100 million monthly active users, carried here by PYMNTS. Sacra estimates $500M annualized by April 2026, up from $232M in 2025.
A well-funded company can still lose a motion. The thing that could plausibly take retrieval away is an adverse injunction on what the engine may fetch, which is the kind of remedy Amazon already won a preliminary injunction for over the agentic shopping tool.
So the uptime question resolves to a boring yes. Every page ranking for "is perplexity down" is a status dashboard answering that one binary, and it answers it correctly. If you ship a feature on the API, your interest runs past the binary.
A retrieval vendor has a second surface that no status page covers: the corpus it can read. Your integration does not consume Perplexity's servers. It consumes whichever publishers those servers were allowed to fetch this week.
TL;DR: Availability and citation coverage are separate failure classes. Only one of them has a dashboard.
Search perplexity lawsuit and every ranking page hands you a different list, several of them with a confident total attached. I could not find a single source that supports any total, so this post does not print one. Each docket gets checked on its own.
Read that last row twice. Dockets move in both directions, which is the practical reason a tally published in June is fiction by August, including any tally you find here.
For a developer, only one column of that roster matters: which of those plaintiffs owns pages your feature currently depends on. If your answers lean on news copy, forum threads or product listings, the pressure is pointed at your source set, not at your uptime. The same dynamic runs through what actually changed for scraping in the AI age.
The suits: what a developer actually needs from the docket
Record the domains behind every answer you ship. Then Reddit thinning out of your source set reaches your own logs before it reaches a headline, which is the whole plan available to you. Perplexity has not said either way: users put the question directly to the company in its own lawsuit-response thread and got no reply. Reddit's First Amended Complaint of February 9 2026 also names Oxylabs, AWMProxy and SerpApi, so the pressure sits on the scraping supply chain and not on one crawler.
One has. The Noel privacy class action against Perplexity, Meta and Google, case 3:26-cv-02803-VC, ended on May 1 2026 with a voluntary dismissal without prejudice. Without prejudice means the same claims can be refiled.
Here is the causal hinge. A publisher does not sue its way into your response payload. It gets there by making itself unreachable, through a Cloudflare rule or licensing terms that say no, and I have covered how blocking AI crawlers works from the publisher side already.
What nobody writes about is the downstream half. Follow the request path.
Where a publisher block lands in your request path
Notice where the diagram does not fork. There is no error branch. A dropped publisher shrinks the reachable source set, the answer gets composed from whatever survived, and your client receives a 200 with fewer citations behind it.
Blocks are also leakier than they look. Commenters in Perplexity's own lawsuit-response thread pointed out that an assistant riding an already-logged-in browser tab walks straight past an anti-bot rule. So a block narrows what gets crawled without closing the door, which is precisely why the effect on you is gradual and unannounced.
One popular explanation needs ruling out before you go further: that the engine invents links. It mostly does not.
Ahrefs tested roughly 9.8 million cited URLs and found Perplexity's cited links 404 at 0.87%, against 0.84% for Google's own search results. ChatGPT was the outlier at 2.38%.
Be careful what that licenses. A 404 rate over cited URLs is not a hallucination rate, because a fabricated URL sitting on a real domain can return a soft 404 or a redirect and gets counted as alive.
What the number does license is narrower and still useful. Cited links resolve about as often as Google's own results, so broken citations are not the anomaly here. Status codes cannot separate a fabricated URL from a dead one, which is also the honest reason the verifier in the last section needs an archive check.
Developers hitting this show up in the forums searching some version of perplexity api citations missing, and the reports cluster into two shapes. Both are user-reported across independent threads with no vendor confirmation either way, so read them as behaviours to instrument, not as documented defects.
Start with the mundane explanation, because it covers part of the gap. The UI's Pro and Deep Research modes are multi-step by design, so UI domain counts were never the number to hold a single API completion to. Fewer domains through the API is partly the predicted result of a different pipeline, and UI counts were never the number to hold the API to.
search_results
object for a given domain, on an otherwise successful response.The instinct at this point is to pin the corpus down with an allowlist. That instinct has its own forum thread, titled search_domain_filter not working - getting citations outside my domain list, and the reports there describe citations arriving from outside the list. Others describe the filter coming back with empty citation fields on sonar-pro.
The filter does work for plenty of callers, which is the point. It is a supported, documented parameter that a couple of unreproduced threads say can lapse, so it is a guarantee you should not lean the whole feature on.
Which leaves you one honest move. Stop trying to make the response guarantee something, and assert the guarantee yourself.
import { metrics } from "./metrics";
const MIN_SOURCES = 3; // the floor the feature was actually built against
export class CitationFloorError extends Error {}
export type Source = { url: string; title?: string };
export type Answer = { text: string; sources: Source[]; belowFloor: boolean };
export async function ask(
query: string,
opts: { throwBelowFloor?: boolean } = {},
): Promise<Answer> {
const res = await fetch("https://api.perplexity.ai/chat/completions", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.PPLX_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: "sonar-pro",
messages: [{ role: "user", content: query }],
}),
});
if (!res.ok) throw new Error(`perplexity http ${res.status}`);
const body = await res.json();
// Never index blindly into a payload shape you do not control.
const text = body?.choices?.[0]?.message?.content;
if (typeof text !== "string") throw new Error("perplexity: no completion in body");
// The old `citations` field was removed, so there is nothing to fall back to.
const sources: Source[] = body.search_results ?? [];
const belowFloor = sources.length < MIN_SOURCES;
if (belowFloor) {
// A 200 carrying too few sources is a failure only your code can name.
metrics.increment("pplx.below_floor", { sources: sources.length });
// Batch and offline callers opt in to hard failure. Request paths do not.
if (opts.throwBelowFloor) {
throw new CitationFloorError(
`${sources.length} sources for ${JSON.stringify(query)}, floor is ${MIN_SOURCES}`,
);
}
}
return { text, sources, belowFloor };
}
The floor is a number you pick from your own testing. No vendor promised it, and no response will ever raise it for you.
Returning belowFloor
instead of throwing keeps the decision where the context lives. A user-facing route can still render a thin answer with a warning, and a batch job passes throwBelowFloor
to abort loudly. Turning two decent sources into a 500 and an empty screen is strictly worse for the person waiting.
If you see zero sources and your code looks correct, check your wrapper before you blame the API. Both LangChain and Open WebUI have reported cases of citation data getting dropped between the response and the caller.
Fewer sources, or none: what developers report and what to do about it
Partly because they are not the same pipeline. The UI's Pro and Deep Research modes fan one query into several searches and browse steps, while a single sonar-pro completion is one retrieval pass. Developers do report gaps wider than that explains: in one forum case, 21 domains surfaced in the web UI while the API produced citations for 6 of them on the same query. Perplexity has not acknowledged it, and one case is not a measured rate. Read the length of search_results on every call and store it next to the query, so your own numbers settle the question.
Yes, and it arrives as a successful response, so nothing in the payload announces it as a failure. Developers report the API suddenly stopping returning a search_results object for one domain, with no vendor acknowledgement. The older citations field is a second route to an empty answer: it was deprecated and removed, so wrapper code still reading it finds nothing there. Check for an absent or empty search_results in your own code, and decide there what a thin answer should do.
A floor check catches the cliff. It will not catch ai citation drift, which is the slow version: the count holds steady while the identities underneath it rotate. The dashboard that answers is perplexity down has no column for this.
Published drift measurements are cross-engine, not Perplexity-specific, and that scope matters more than the digits. SISTRIX puts cross-engine drift at 85% per week at the URL level and 74% at the domain level. An independent analysis from Machine Relations puts cross-engine domain change at 40-60% month over month for identical queries.
Those two cannot both be measuring the same quantity. A 74% weekly churn compounds to near-total monthly turnover; 40-60% a month does not. The metrics, query sets and windows must differ enough that neither is a number to plan against. Nobody holds a stable baseline, which makes your own delta the only readable drift figure you will get.
It takes about fifty lines. Freeze a query set that mirrors your production traffic, re-run it weekly, and diff the domains.
import { readFileSync, appendFileSync, existsSync } from "node:fs";
import { ask, type Source } from "../lib/perplexity";
// A frozen set. Changing it invalidates every week you already recorded.
const QUERIES: string[] = JSON.parse(readFileSync("golden-queries.json", "utf8"));
const LOG = "drift.jsonl";
const SAMPLES = 3; // one sample per query cannot see past the noise floor
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
type Run = { week: string; query: string; domains: string[]; belowFloor: boolean };
const week = new Date().toISOString().slice(0, 10);
const history: Run[] = existsSync(LOG)
? readFileSync(LOG, "utf8")
.split("\n")
.filter(Boolean) // an empty or trailing-newline log must not throw here
.map((l) => JSON.parse(l) as Run)
: [];
const hosts = (sources: Source[]) =>
sources.map((s) => new URL(s.url).hostname.replace(/^www\./, ""));
for (const query of QUERIES) {
const seen = new Set<string>();
let belowFloor = false;
for (let i = 0; i < SAMPLES; i++) {
try {
const answer = await ask(query);
belowFloor = belowFloor || answer.belowFloor;
for (const host of hosts(answer.sources)) seen.add(host);
} catch (err) {
// A thin or failed sample is a recorded data point, not a reason to stop.
console.warn(`${query} | sample ${i} failed: ${(err as Error).message}`);
}
await sleep(2_000); // re-running a golden set back to back trips 429s
}
const domains = [...seen];
const prior = history.filter((r) => r.query === query).at(-1);
if (prior && prior.domains.length > 0) {
const survived = domains.filter((d) => prior.domains.includes(d));
const lost = prior.domains.filter((d) => !domains.includes(d));
const survival = survived.length / prior.domains.length;
console.log(
`${query} | survival ${(survival * 100).toFixed(0)}% | lost ${lost.join(", ") || "none"}`,
);
}
appendFileSync(
LOG,
JSON.stringify({ week, query, domains, belowFloor } satisfies Run) + "\n",
);
}
Alert on survival, not on averages. A sparse week is a value to log, not an outage, which is why the loop records an empty domain list and keeps going instead of letting one thin query abort the whole run.
An absolute four-week rule does not survive the arithmetic. At the 74% cross-engine domain-level churn SISTRIX measured, a domain vanishing from four consecutive single-sample runs is roughly a 30% event with zero corpus change, so "gone four weeks running" fires false alarms across any real query set. Sample each query three to five times per run, spend the first four to six weeks building your own per-domain survival band, and alert on deviation from that band.
Then hold the conclusion loosely. Model deprecation, index and reranker changes, and licensing deals that re-open access all move the same needle.
What the harness earns you is the fact that your source set moved. Attributing that move to litigation is a hypothesis you still have to test.
Drift tells you the set moved. This tells you whether what came back holds up, and developers who go looking for it search "verify ai citations" and mostly get sold another model call. You do not need one.
Start from the rule Perplexity's own docs state: never ask the model to generate source URLs, always read search_results
. Model-written URLs never enter this flow. Everything downstream is then two questions asked in order.
The split is what makes the results readable. A 404 with an archive snapshot is a page that existed and went away, which is a content problem. A 404 with no snapshot has no record of ever existing.
One note on that third name. Because every URL in this flow comes from search_results
and never from the model, LIKELY_HALLUCINATED here means gone with no archive record. The label carries over from work on pipelines that do accept model-written URLs.
type Resolution = "LIVE" | "DEAD" | "LIKELY_HALLUCINATED" | "BLOCKED" | "UNKNOWN";
type Support = "SUPPORTED" | "UNSUPPORTED";
type Stage1 = { resolution: Resolution; html?: string };
// A real agent string with somewhere to complain to. Not optional at any volume.
const UA = "citation-verifier/1.0 (+https://example.com/bots)";
// Stage 1: does the URL resolve? Returns the body so stage 2 never refetches it.
async function resolveUrl(url: string): Promise<Stage1> {
let res: Response;
try {
res = await fetch(url, { redirect: "follow", headers: { "user-agent": UA } });
} catch {
return { resolution: "UNKNOWN" }; // DNS, TLS or timeout: a human looks, we do not guess
}
// Challenge pages and rate limits are their own state, not an unknown.
if (res.status === 401 || res.status === 403 || res.status === 429) {
return { resolution: "BLOCKED" };
}
if (res.status === 200) {
try {
return { resolution: "LIVE", html: await res.text() };
} catch {
return { resolution: "UNKNOWN" };
}
}
if (res.status !== 404) return { resolution: "UNKNOWN" };
try {
const wb = await fetch(
`https://archive.org/wayback/available?url=${encodeURIComponent(url)}`,
{ headers: { "user-agent": UA } },
);
const snap = (await wb.json()) as { archived_snapshots?: { closest?: unknown } };
// Archived means it was real and died. Nothing archived means no record at all.
return {
resolution: snap.archived_snapshots?.closest ? "DEAD" : "LIKELY_HALLUCINATED",
};
} catch {
return { resolution: "UNKNOWN" }; // archive.org being down is not a verdict
}
}
// Stage 2: does the page carry the claim? Plain string comparison, no inference.
function carriesQuote(html: string, quote: string): Support {
const text = html
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.toLowerCase();
return text.includes(quote.trim().toLowerCase()) ? "SUPPORTED" : "UNSUPPORTED";
}
export async function verify(url: string, quote: string) {
const { resolution, html } = await resolveUrl(url);
// Only LIVE reaches stage 2, and it reads the body stage 1 already has.
const support = resolution === "LIVE" && html ? carriesQuote(html, quote) : null;
return { url, resolution, support };
}
Your verifier cannot inherit the failure mode it exists to catch. Every step above is an HTTP request, an archive lookup or a string comparison, and no model is asked to judge anything.
That makes it cheap enough to run over every citation you ship, just not in the request path. Verify after the answer is served, cache each URL's resolution with a TTL, cap concurrency, and send a real User-Agent with a contact URL on it.
The four states in the diagram are the core set. A deployment needs a fifth, because a verifier pointed at the same publishers this post says are deploying Cloudflare rules will collect 401s, 403s and challenge pages in bulk. Those are BLOCKED, they are expected, and they are not UNKNOWN.
Two things no status code can see. A site serving a 200 for a missing page classifies LIVE and then UNSUPPORTED, so soft 404s look exactly like a publisher who dropped your quote. Exact substring matching is strict as well, so normalize whitespace and quote characters, and read UNSUPPORTED as "go look" instead of "the engine lied".
So do not rip the dependency out over headlines. The company is not going dark, and my argument against migrating is that another retrieval vendor hands you the same corpus-reachability risk with a smaller pile of public reporting to read about it. Owning the retrieval layer yourself does genuinely remove the corpus-opacity problem, at the cost of building and feeding one.
Ship three things this week instead. A source-count floor your own code raises, a weekly drift log over a frozen query set, and the two-stage classifier on the citations you show users.
Then the next time a headline sends you hunting for a status page, the better answer is already sitting in your own logs. Not down. Citing a narrower set of domains than it did last month, which is the signal worth acting on.