Email trained people to hesitate. Chat trained them not to. A link that arrives inside a thread they were already reading, from a name they already know, gets tapped before the preview has finished loading. This page is about where a platform can put a hostname check — at send, at unfurl or at render — and how to do it inside a latency budget that leaves nothing to spare.
The question is not whether to check. It is which stage of the message lifecycle the check belongs to.
Compose is too early: the user has not committed, and warning them about their own paste is noise. Tap is too late for most platforms, because the operating system has already handed the URL to a browser you do not control. The three in the middle are where real products put the check.
Checking at send has one property nothing else can match — it stops the message rather than warning about it. Catch a hostile host before fan-out and nobody in a five-thousand-member channel ever sees the link, with no window between delivery and moderation for it to do its work. That is the right place for a platform that controls the server pipeline and does not promise end-to-end encryption for the conversation in question.
The service generating the link preview has to resolve the URL, follow redirects and fetch metadata anyway, so adding a hostname check costs one call on a path that is already asynchronous and already tolerant of latency. It also lands the verdict exactly where attention is: the preview card is what people look at, so replacing it with a warning beats appending a notice underneath the message.
If the server genuinely cannot read the message, the check has to happen where the plaintext exists, which is the recipient's device. That pushes the design toward a downloaded list rather than a per-message query, and it changes the engineering problem from "call an API" to "keep a compact local dataset current". More work — and for a platform whose selling proposition is privacy, the only honest option.
Send-path checking for unencrypted channels and bot traffic, unfurl checking as a second pass that catches whatever the send path could not resolve, and a retrospective sweep over recent messages when the daily list changes. The three are not redundant: they cover different failure modes, and the third exists because a link that was clean on Tuesday can be verified as hostile on Wednesday.
End-to-end encryption is a promise about what the server cannot see. Link checking that quietly breaks it is worse than none.
The engineering conversation about phishing in an encrypted messenger tends to go wrong in the first five minutes, because someone proposes server-side URL scanning and someone else points out that this requires the server to read messages. Both are right, and the resolution is not a compromise on encryption — it is moving the check to the client and accepting the constraints that come with it. Those constraints are real, and they are also narrower than people expect.
Downloading a list of hostnames and matching locally is the whole design: the message never leaves the device, the hostname never leaves the device, and no per-message lookup exists to be logged, subpoenaed or correlated. The full database ships as CSV or JSON, downloads are unlimited on a subscription, and a client can pull it on its normal update schedule.
With a downloaded list, checking a link reveals nothing about the user to anyone — not to you, not to the data provider, not to a network observer. With per-message API lookups, someone somewhere accumulates a record of which hostnames your users' messages contained. For a product chosen specifically because of what the server cannot see, no privacy policy repairs that difference.
If your infrastructure can already read message content, sending a hostname to a checking service adds no new disclosure about content you did not already hold, so per-query checking is a reasonable trade. Even then, high-volume platforms usually move to the local list anyway — at pipeline scale, an in-memory set-membership test beats a network round trip on every axis, including the ones finance cares about.
Email phishing is a broadcast. Chat phishing is a chain reaction, and that is why a delayed response costs so much more here.
One member's credentials are harvested elsewhere, and the attacker then posts from inside. Every recipient sees a colleague's name, a real avatar and a thread they were already following. No header analysis helps, because nothing about the message is forged.
An invite link drops a user into a channel populated with plausible-looking members and a pinned message containing the payload. The social proof is manufactured, cheap, and considerably more persuasive than any email pretext.
Bots post at machine speed and are often exempted from the rate limits and moderation heuristics applied to humans. A compromised or malicious integration can seed hundreds of channels before anyone reads one of them.
SMS and RCS carry the delivery-notice and bank-alert lures, almost always behind a shortener because of the character limit. Resolve the redirect chain first and check the final hostname, not the shortener's.
A shared document, spreadsheet or slide deck carries the URL inside the file rather than in the message body. A scanner that only reads message text sees nothing at all, which is exactly why the technique persists.
A benign host is shared today and turned hostile in three days, after any send-time check has already passed. Only a retrospective sweep against the updated daily list closes this one.
The last of those deserves emphasis because most implementations forget it. A check at send time is a statement about that moment only, and attackers know it — staging the infrastructure through register, park, share, then activate is a well-worn way to defeat point-in-time scanning across every medium. The counter is cheap: keep an index of hostnames shared in the last few weeks, and each morning join it against the newly-added entries in the daily changelog, then replace the preview, disable the link or append a warning depending on how aggressive your product wants to be.
Users notice a message that takes longer than about a tenth of a second to appear sent. Everything you add has to fit inside what is left.
Work the budget backwards. A user tolerates roughly a hundred milliseconds between tapping send and the message appearing as delivered before the interface starts to feel sticky, and out of that you have already spent time on transport, authentication, persistence and fan-out. Whatever a security check consumes comes out of the remainder — and if it does not fit, the check gets moved off the critical path, which is the correct engineering decision and also the exact moment the control stops preventing anything.
Up to a hundred domains go in a single POST at one lookup each, with the response carrying checked, phishing_found and credits_used. Deduplicating hostnames before the call helps more than it sounds, because a message pasting the same host five times is one lookup rather than five.
Ten requests per second per key is generous for a checking service and immediately inadequate for a pipeline handling millions of messages a minute. That gap is not a limitation to route around; it is a signal about which product to use. Download the full database, hold it beside the message handler, and every check becomes a set-membership test measured in microseconds.
There are exactly two confidence values in a response — 0.98 when a match is confirmed and 0.0 when there is none — which removes any per-surface threshold and any ambiguous middle band to argue over. Everything else is equally unambiguous: is_phishing answers yes or no, category never varies from phishing/malware, dns_status reports resolves, and last_checked gives you a day you can show a user.
A badly written warning trains users to dismiss warnings, and every security feature you ship afterwards inherits that damage.
Most in-app phishing warnings fail for the same reason browser certificate warnings failed for a decade: they are vague, they are frightening, and they offer a "continue anyway" button that becomes muscle memory within a week. The purpose of the screen is not to express severity — it is to give the user one specific fact they did not have, phrased so that the right choice is the obvious one rather than merely the cautious one.
"This address has been verified as an active credential-harvesting site" tells the user something checkable. "This link may be unsafe" tells them nothing and invites them to make their own assessment, which is precisely the assessment the tool exists to spare them. If you hold a last_checked date, show it — recency is what makes a claim like this credible to a sceptical user.
A confirmed match against a verified list is a much stronger signal than a heuristic, and it justifies a harder stop than most warnings can. If the product needs an override for legitimate reasons — a researcher, a journalist, a moderator inspecting a report — make it deliberate rather than a same-size button beside the safe one, and log it when it is used.
In a group context, notify the sender as well as the recipients. An account distributing hostile links is far more often compromised than malicious, and the person who owns it needs to know before they lose considerably more than a channel. That notification is also the cheapest account-takeover signal your safety team will ever get.
When the same hostile host is posted forty times across a channel, forty interstitials is a denial-of-service on your users' attention. Collapse them into one thread-level notice and one moderation event, and let the abuse tooling handle the rest quietly in the background.
The destination behind this link was confirmed as an active credential-harvesting page designed to look like a sign-in screen. Nothing about your message or your account was shared to determine this — the address was matched against a list already stored on this device.
Stated plainly, because a platform team will discover it in week two and would rather have been told in week one.
This is a known-bad lookup and nothing more. When a hostname comes back clean, the only thing you have learned is that it has not been seen and verified — a much weaker statement than "safe", and one that will never become stronger. Infrastructure stood up forty minutes ago and fired into a single chat thread has had no opportunity to be observed by anybody, so it is simply not in the file, and that is a property of verification itself rather than a gap somebody forgot to close. Any product claiming otherwise is describing a heuristic and calling it a fact.
Checking a shortener's own hostname tells you about the shortener and nothing about where it goes. You have to follow the chain first, which is work your unfurl service is probably already doing and your send path probably is not. On SMS and RCS surfaces, where shorteners are effectively mandatory because of message length, skipping this step makes the whole check decorative.
A URL inside a shared document is not in the message body, so anything reading only message text sees nothing. That is exactly why the technique persists long after message scanning ships. Extraction has to happen in your upload pipeline — or, on an encrypted platform, client-side alongside the local match.
The commodity kits, the reused templates, the infrastructure rotating through a campaign that also hit four hundred other services this week — that is the traffic actually flooding a group chat. Removing it means your abuse team and your users spend attention on the small residue that is genuinely novel, alongside the approaches on our URL shorteners, social media and identity protection pages.
Targeted checking — reported messages, new-account first posts, bot traffic, moderation queues — is credit-shaped work, and credits run through PayPal on a twelve-month expiry: 10,000 lookups for $59, 25,000 for $99, 100,000 for $249, 250,000 for $499, and larger tiers beyond. Once every message is in scope, the Daily Threat Feed at $499/month is the right shape, because downloads are unlimited and the per-check cost falls to zero.
Eight questions, including the one about whether this is compatible with the privacy claim on your marketing site.
Yes, and it is the intended pattern rather than a workaround: the client downloads the phishing domain list on its normal update cycle and matches locally against hostnames it already holds in plaintext, because it is the endpoint — the message is never decrypted anywhere else, no hostname is transmitted for checking, and the server learns nothing it did not already know. What you do need to get right is distribution, because your own backend must authenticate to the feed and republish the list to clients rather than embedding a key in an app binary, and that indirection also lets you ship a compact derived structure instead of the raw file if device storage is a constraint.
If you can read message content, put the primary check in the send path before fan-out, because blocking beats warning and it protects everyone in a channel at once; then add a second pass in the link-preview service, which is already resolving the URL and following redirects and is the natural place to catch whatever the send path could not resolve; then run a nightly retrospective job over recently shared hostnames against the daily changelog. If you cannot read message content, the send and unfurl stages are unavailable and everything moves to the client at render time. Do not make tap time your only check — on most platforms the operating system has already handed the URL to a browser you do not control.
At single-message volumes, no — responses come back in under 50 milliseconds and a whole message's hostnames go in one batched request rather than one per link, so even a link-heavy message is a single round trip. Two things keep it honest in production: deduplicate hostnames before calling, and set an aggressive timeout with a defined fallback, which for most consumer products means failing open and letting the unfurl pass and the nightly sweep pick up the slack. Once you are past a few thousand messages per second the answer changes entirely, and the correct move is to stop calling anything and hold the list in memory beside the handler.
Resolve first, then check: a shortener's hostname is not the destination, and checking it tells you only that the shortener is not itself a phishing domain, which is almost always true and almost never useful. Follow the redirect chain server-side with a hop cap and a timeout, check the final hostname, and check any intermediate hosts too if the chain crosses several parked domains — which is one reason the unfurl service is such a good integration point, since it already does that resolution work to build the preview card. On SMS and RCS surfaces, where shorteners are effectively mandatory because of message length, this step is not optional at all.
A hostname check cannot help until something extracts the hostname, and that extraction is on your side of the boundary: if your platform already processes uploads for previews, thumbnails or text indexing, that pipeline is where URLs should be pulled out of documents and batched through the same check as message text. If it does not, this gap stays open and you should say so internally rather than assume message scanning covers it, because the technique specifically exists to defeat scanners that read only message bodies. On an encrypted platform the extraction has to happen client-side alongside the local match, which is more work but architecturally identical.
Check the hostname immediately, because a confirmed match turns a judgement call into a fact and lets an automated action fire without a human in the loop. A verified result also lets you act on the whole blast radius rather than the single reported message — find every occurrence of that hostname in the recent window, disable the links, and notify the accounts that posted them, which are usually compromised rather than hostile. Keeping a bounded index of shared hostnames for a few weeks is what makes that possible, and it is the same index the nightly changelog sweep already uses.
Only if you deploy it as per-message cloud lookups on an encrypted product, which is the deployment to avoid for exactly that reason. With the downloaded feed, no query about any user's message ever leaves the device or your infrastructure, and the only outbound traffic is your own scheduled download of a threat list that reveals nothing about anybody. A researcher who decompiles your client and watches its network traffic will find that this holds, which is a far stronger position than a paragraph in a policy document — so write it into your transparency documentation in plain words: the app downloads a list of known phishing addresses and compares links to it locally.
The database is rebuilt every 24 hours, the daily build lands at 04:30 UTC, and inclusion requires a verified active A record confirmed through rotating proxy infrastructure. Anything that stops resolving is dropped at the next rebuild instead of lingering, so the file stays a working picture of what is live — which matters a great deal for a client-side deployment where every kilobyte is shipped to a phone. Because a changelog of additions and removals ships alongside, a client can apply a delta rather than pulling the whole file again, and because /stats is open — no key, no credits, no auth — your build pipeline can assert on database size and last-update date at no cost at all.
Confirm the database size through the open statistics endpoint, wire one batched call into the unfurl service, and measure what it costs you in milliseconds before committing to anything. If every message is in scope, the downloaded feed is the shape that keeps latency flat and keeps user lookups off the network entirely.