We follow magic link email best practices because we have to live with them: GetFluxly uses email-first, passwordless sign-in, so every account holder logs in through a link we send ourselves. That means every rough edge in the flow lands in our own support inbox first. This post is what we learned building and running it: how to write a subject line that gets found in a crowded inbox, how to think about expiry and single-use tokens, the production failure mode where automated scanners spend a link before the human does, what to do when the link opens on the wrong device, and the cases where a magic link is simply the wrong tool.
Why we send login as email in the first place.
A magic link is a login credential you deliver by email. The user types their address, we send a message with a link, they click it, and they are in. No password to remember, forget, reuse, or leak. For a small SaaS product that is a genuinely good trade: fewer credentials to store, no password reset flow to build and maintain, and a lower support burden from locked-out users. The catch is that you have now made your login experience depend on transactional email working perfectly, which is a higher bar than most teams expect. Everything below follows from taking that bar seriously.
Because a magic link is transactional email at its most consequential, the general rules for the category all apply here first. If you have not read it, our guide to transactional email best practices covers the foundation this post builds on.
The three things the email itself has to get right.
Before any of the security questions, the email has to function as a piece of communication. These are the three we treat as non-negotiable, and they are all about getting the person into the product with the least friction possible.
01. Say exactly what the email is for.
Our sign-in subject line is simply 'Sign in to GetFluxly.' There is nothing clever in it because cleverness costs recognition, and recognition is the whole job. The person requested this email seconds ago and is scanning a crowded inbox for it under mild time pressure. A subject that names the product and the action is found instantly. A subject that tries to be warm or witty makes the reader stop and decode, which is the opposite of what a login email should do.
02. Keep the credential out of the subject.
If you send a one-time code, the code never belongs in the subject line. Subject text is rendered on lock screens and in notification previews, so a code in the subject is a code anyone glancing at the phone can read. We keep codes inside the body only, and the subject for that email is 'Your GetFluxly sign-in code' with no digits attached. The same caution applies to any sensitive value: the subject line is effectively public, so treat it that way.
03. Lead with the button, keep the fallback close.
The link is the email, so the button comes first, right under the heading, not after a paragraph of product copy. Directly beneath it we show the same URL as plain, copyable text, because some clients strip buttons, some users are on a device where the button does not behave, and a visible URL is a reliable escape hatch. Everything else, the expiry note, the reassurance line, the safety phrase, sits below the primary action where it supports without competing.
Expiry and single use are UX decisions, not just security ones.
A magic link should expire quickly and work exactly once. We use a 15-minute window, and we make the link single use: the moment it is consumed, or the moment the user requests a fresh link or code, the old one is dead. Requesting a new code deterministically invalidates the prior link, so there is never a pile of live tokens sitting in an inbox waiting to be found. This is the security floor for emailing a credential at all.
The part teams underestimate is that both of these choices are felt by the user, not just by the threat model. If the window is too short, a person who steps away for coffee comes back to a dead link and has to start over. If the link is single use but you resolve it the instant the page loads, a double-tap or a browser that prefetches the destination can burn it. So the copy has to be honest about the window, the expired-link screen has to make requesting a new one a single obvious action, and the resolution logic has to be careful about what counts as a real use. Get the security right, then make the failure paths gentle.
The link scanner problem nobody warns you about.
Here is the failure mode that will blindside you in production. You ship single-use magic links, they work perfectly in testing, and then reports start coming in from users at larger companies: the link is expired the moment it arrives. Nobody clicked it. What happened is that a corporate email security product, Proofpoint, Mimecast, Barracuda, Microsoft Safe Links, and others like them, fetched every URL in the incoming message to scan it for malware. That automated fetch spent your single-use token before the human ever saw the email. The security tooling meant to protect the recipient just locked them out of your product.
There are three honest ways to handle this, and they are not mutually exclusive. The first is to allow the token to be used a small number of times within a short window instead of exactly once, so a scanner's fetch plus the human's real click both succeed while the link still dies quickly. The second is to not resolve the login on page load at all: the link opens a page that says "Confirm sign-in" and only spends the token when the person clicks that button, which a headless scanner will not do. Both work, and both are reasonable.
The approach we chose is a third one: put the token in the URL fragment, the part after the #, instead of the query string. A fragment is never sent to the server. It stays in the browser. So when a scanner makes its server-side request to our confirm URL, the token simply is not in the request it sends, and there is nothing for it to consume. The real browser, on the other hand, keeps the fragment, reads it in client-side JavaScript, and resolves the login. This is what makes it safe for us to auto-resolve on open for humans while remaining immune to the scanners, and it is the single most useful thing we figured out in this whole project. As a bonus, a fragment does not leak through the Referer header or into server logs the way a query-string token would.
One implementation note if you go this route: read the token from the fragment and then immediately scrub it out of the address bar so it does not linger in browser history, and lock the confirm route down with a strict referrer policy and framing headers. The token is sensitive right up until it is spent, and treating it that way end to end is what keeps the convenience from turning into an exposure.
Whether to include a copy-paste code as a fallback.
The common question is whether to put a numeric code in the email alongside the link, so a user who cannot click can still get in. Our answer is to keep them as two distinct emails rather than crowding both into one message. By default we send the link email. When someone chooses "enter a code instead" on the request screen, or when a click lands in a different browser than the one that asked to sign in, we send a dedicated code email with the digits shown large and clear in the body.
Keeping them separate keeps each email honest about its one job. The link email is a single tap. The code email is a value you type. Mixing them produces a busier message with two competing calls to action, and it tempts you into the mistake of echoing the code somewhere unsafe. The rule we hold without exception: the code never appears in the subject line, because subject text is visible on lock screens and notification previews. A credential that shows up on a locked phone screen, before the owner has even opened it, is not a credential you control.
When the link opens on the wrong device.
Device mismatch is the other common real-world snag. Someone requests the link on their laptop, then opens the email on their phone because that is where their mail app lives, and now the session is being established on a different device than the one they were trying to use. Or the link opens in the default browser when they had started in a different one. This is not an error, exactly, but if you assume the request and the click happen in the same place, you will hand people a confusing result.
We handle it two ways. The click resolves the login wherever it lands, so opening on the phone signs you in on the phone rather than failing, and if the person actually needed the laptop session, the code path lets them carry the sign-in across by typing the code into the original screen. Alongside this we show a short safety phrase, a two-word pair like "amber otter", on both the screen where the login was requested and inside the email. If the phrase matches, the person knows the email they are looking at belongs to the request they just made, which quietly defends against a whole class of phishing where someone is talked into reading out a link or code that was triggered by an attacker.
A note on rendering, because a login email that clips is a broken login.
One small but real lesson: our sign-in email uses a web-safe font stack rather than a hosted webfont, and we keep the whole message well under the size where Gmail clips long emails behind a "view entire message"link. A login email is the worst possible place to have your button fall below a clip boundary or render in a fallback font that breaks the layout, because the person is trying to get in right now. Plain, table-based, web-safe HTML is not glamorous, but it renders the same in Outlook, Apple Mail, and Gmail, and for a message this important that consistency is worth more than a custom typeface. The same deliverability fundamentals that keep any transactional email in the inbox apply doubly here; our guide to email deliverability for SaaS covers the authentication and reputation side of keeping these messages out of spam.
When magic links are the wrong choice.
We like magic links, and we still tell people not to use them in the wrong places. They rest on one assumption: that a single person reliably controls the inbox and can get to it quickly. Break that assumption and the method breaks with it. Shared team inboxes and role addresses like support@ mean the "one person, one inbox" model no longer holds. Users who sign in many times a day find the trip to their email tedious next to a password manager that autofills. And if your email deliverability is shaky, a login that depends entirely on a message arriving within a minute turns every delivery hiccup into a locked-out customer.
The honest position is that magic links are one option among several, not a replacement for all of them. For high-frequency logins, offer a password or a passkey as well. For teams and enterprises, expect to support SSO. For everyone, have a graceful answer for the moment the email does not arrive, which is usually a code path or a fallback contact method. If you do adopt magic links, the same discipline you bring to password reset emails carries straight over; see our companion guide to password reset email best practices for the sibling flow.
Magic link emails, answered.
How long should a magic link stay valid?
Short. We use 15 minutes, and that has held up well in practice. A magic link is not a password you keep; it is a one-time key the user is actively waiting for, so the window only needs to be long enough for the email to arrive and the person to open it. Anything longer than about half an hour is a security cost with almost no usability benefit, because a link that sits unused for an hour is more likely to have been abandoned than genuinely needed. Whatever window you pick, state it in the email in plain words: 'This link expires in 15 minutes' is useful, 'this link may expire' is not.
Should a magic link be single use?
Yes, a magic link should be consumed once and then be dead. Single use is what makes it safe to email a login credential at all: even if the message is later exposed, the token inside it no longer works. The wrinkle is that automated systems can consume a single-use link before the human does, which is the link-scanner problem covered in this post. The fix is not to abandon single use; it is to make sure only a real click from a real browser can spend the token, which we do by carrying the token in the URL fragment where server-side scanners never see it.
Why do magic links sometimes stop working before the user clicks them?
The usual culprit is an automated link scanner. Corporate email security products (and some mailbox providers) fetch every link in an incoming message to check it for malware, and if your magic link is single use and the token lives in a normal URL, that automated fetch spends the token before the human ever clicks. The person then arrives at an 'expired link' screen through no fault of their own. Mitigations include allowing the link to be used a small number of times within a short window, requiring a confirmation click on the destination page rather than resolving on load, or putting the token in the URL fragment so it is invisible to any server-side request.
Should I include a copy-paste code as well as a link?
It depends on the situation, and the cleanest approach is to treat the link and the code as two separate emails rather than one email with both. We send a link email by default and a code email when the person asks to enter a code instead, or when a click lands in a different browser than the one that made the request. The one hard rule we hold: never put the code in the subject line, because subject text shows up on lock screens and notification previews where anyone can read it. A code is a credential and should stay inside the body of the message.
When are magic links the wrong choice?
Magic links are a poor fit when the user does not reliably control the inbox, when they sign in many times a day, or when email delivery itself is shaky. Shared team inboxes, role addresses like support@, and accounts where several people need access all break the one-person-one-inbox assumption. People who log in constantly find the round trip to their email tedious compared to a password manager autofill. And if your transactional email deliverability is not solid, a login method that depends entirely on an email arriving within a minute will generate support tickets. In those cases, offer passwords, passkeys, or SSO alongside or instead of magic links.
The thing we did not expect, going in, is how much of magic-link quality lives outside the cryptography. The token math is the easy part. The hard part is the subject line a stressed person can find, the expiry that is short without being punishing, the scanner that spends your link, and the honest admission that some users should not be on magic links at all. Get those right and passwordless sign-in feels effortless. Get them wrong and it becomes the most frustrating screen in your product.