<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sadeq Sheikhi</title>
    <description>The latest articles on DEV Community by Sadeq Sheikhi (@sadegh_shaikhi_0549a5c17f).</description>
    <link>https://dev.to/sadegh_shaikhi_0549a5c17f</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2223603%2Fb8de485d-8767-4631-9f51-b5a26eeeb1a2.webp</url>
      <title>DEV Community: Sadeq Sheikhi</title>
      <link>https://dev.to/sadegh_shaikhi_0549a5c17f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sadegh_shaikhi_0549a5c17f"/>
    <language>en</language>
    <item>
      <title>Passkeys Explained: How Passwordless Authentication Works</title>
      <dc:creator>Sadeq Sheikhi</dc:creator>
      <pubDate>Sat, 29 Aug 2026 11:50:02 +0000</pubDate>
      <link>https://dev.to/sadegh_shaikhi_0549a5c17f/passkeys-explained-how-passwordless-authentication-works-75k</link>
      <guid>https://dev.to/sadegh_shaikhi_0549a5c17f/passkeys-explained-how-passwordless-authentication-works-75k</guid>
      <description>&lt;p&gt;Passkeys have been around for a while now. You have probably seen them on larger websites such as GitHub and Google, where you can sign in without typing a password at all.&lt;/p&gt;

&lt;p&gt;And every time I used them, I had basically the same thought: &lt;em&gt;why don't more normal websites use this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wanted passkeys on my own website and products for a pretty simple reason: the experience is just better. You don't have to remember a password, open a password manager, copy anything, or wait for a verification code. You choose your passkey, confirm with your fingerprint, face, device PIN, or another authenticator, and you're in.&lt;/p&gt;

&lt;p&gt;It feels almost suspiciously easy for something that is also, when implemented correctly, significantly more resistant to common attacks such as phishing and credential stuffing than traditional password-based authentication.&lt;/p&gt;

&lt;p&gt;But once I started looking into actually implementing passkeys, I realized there is quite a bit happening underneath that simple fingerprint prompt. There is &lt;a href="https://www.w3.org/TR/webauthn-3/" rel="noopener noreferrer"&gt;WebAuthn&lt;/a&gt;, &lt;a href="https://fidoalliance.org/specifications/" rel="noopener noreferrer"&gt;FIDO2&lt;/a&gt;, public-key cryptography, relying parties, challenges, authenticators, credential IDs, discoverable credentials, browser APIs, and a few important decisions your backend needs to get right.&lt;/p&gt;

&lt;p&gt;So this article is the conceptual part. We are going to look at what passkeys actually are, how they work, which standards are involved, what the browser and authenticator are really doing, what your frontend and backend are responsible for, and what happens in real-world situations such as losing your phone or signing in on another computer.&lt;/p&gt;

&lt;p&gt;In a follow-up article, I will move from the theory to the actual implementation and build passkey authentication with Vue and Nuxt.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are passkeys?
&lt;/h2&gt;

&lt;p&gt;The easiest way to think about a &lt;a href="https://fidoalliance.org/passkeys/" rel="noopener noreferrer"&gt;passkey&lt;/a&gt; is as a replacement for a password that uses the security already built into your device.&lt;/p&gt;

&lt;p&gt;Instead of typing a secret string that both you and the server need to somehow protect, you authenticate using something such as a fingerprint, face scan, device PIN, password manager, or hardware security key.&lt;/p&gt;

&lt;p&gt;Underneath that much nicer user experience, passkeys are built on the Web Authentication API, usually called &lt;strong&gt;WebAuthn&lt;/strong&gt;, and public-key cryptography.&lt;/p&gt;

&lt;p&gt;If you want a more developer-friendly introduction than the raw W3C specification, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API" rel="noopener noreferrer"&gt;MDN Web Authentication API guide&lt;/a&gt; is also a very good reference.&lt;/p&gt;

&lt;p&gt;When you create a passkey, an authenticator creates a cryptographic key pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;private key&lt;/strong&gt; is controlled by the authenticator or passkey provider and is not sent to your application during registration or authentication.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;public key&lt;/strong&gt; is returned to your application and stored on your server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction is the entire foundation of how passkeys work.&lt;/p&gt;

&lt;p&gt;With passwords, the user and the server are ultimately dealing with the same secret. Ideally the server stores only a strong hash of the password, but the user still has to know or possess the original secret and send proof of it during login.&lt;/p&gt;

&lt;p&gt;Passkeys change that model completely.&lt;/p&gt;

&lt;p&gt;Your server does not know the secret used to authenticate. It only knows a public key that can verify signatures created by the corresponding private key.&lt;/p&gt;

&lt;p&gt;If someone steals the public key from your database, they cannot use it to generate valid signatures. It is designed to be public.&lt;/p&gt;

&lt;p&gt;This is one of the reasons a database containing passkey credentials is fundamentally different from a database containing password hashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser, authenticator, and server are different parts of the system
&lt;/h2&gt;

&lt;p&gt;This is where passkeys became much clearer to me.&lt;/p&gt;

&lt;p&gt;When you click something like &lt;em&gt;Create a passkey&lt;/em&gt;, there are actually several different actors involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your application frontend&lt;/strong&gt;, which starts the process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your backend&lt;/strong&gt;, which generates challenges and verifies the final response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser&lt;/strong&gt;, which implements the WebAuthn API and enforces rules around origins and relying parties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The authenticator&lt;/strong&gt;, which holds or controls the private key and performs cryptographic operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user&lt;/strong&gt;, who authorizes the authenticator through a fingerprint, face scan, device PIN, hardware key, or another verification mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser and authenticator are related, but they are not the same thing.&lt;/p&gt;

&lt;p&gt;The browser is basically the bridge between your website and the authenticator.&lt;/p&gt;

&lt;p&gt;Your JavaScript asks the browser to create or use a credential through WebAuthn. The browser checks whether the request makes sense for the current website, communicates with an appropriate authenticator, gets the result back, and returns a WebAuthn response to your frontend.&lt;/p&gt;

&lt;p&gt;When the authenticator is external, such as a hardware security key or another device, communication can happen through the &lt;a href="https://fidoalliance.org/specifications/download/" rel="noopener noreferrer"&gt;Client to Authenticator Protocol, or CTAP&lt;/a&gt;. CTAP is the other major piece of FIDO2 alongside WebAuthn.&lt;/p&gt;

&lt;p&gt;The authenticator is the component that actually controls the credential.&lt;/p&gt;

&lt;p&gt;Depending on the device and setup, that authenticator might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows Hello on a Windows computer.&lt;/li&gt;
&lt;li&gt;Touch ID or Face ID backed by Apple's credential system.&lt;/li&gt;
&lt;li&gt;An Android device using the platform's credential manager.&lt;/li&gt;
&lt;li&gt;A password manager capable of storing and synchronizing passkeys.&lt;/li&gt;
&lt;li&gt;A physical FIDO2 security key.&lt;/li&gt;
&lt;li&gt;A phone being used to authenticate a login on another computer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when people say, "the private key stays on the device," that is useful as a simplified explanation, but the reality is slightly more nuanced.&lt;/p&gt;

&lt;p&gt;The important security property is that &lt;strong&gt;your website never receives the private key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The authenticator or passkey provider controls it and exposes only specific operations such as "create a credential" and "sign this authentication request."&lt;/p&gt;

&lt;h2&gt;
  
  
  Your fingerprint is not your passkey
&lt;/h2&gt;

&lt;p&gt;This is another distinction worth making because the user experience can make it seem like your fingerprint itself is being sent to the website.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;Your fingerprint, face scan, or device PIN usually exists to unlock or authorize the authenticator.&lt;/p&gt;

&lt;p&gt;For example, imagine that your laptop contains a passkey for &lt;code&gt;example.com&lt;/code&gt;. When you sign in, the website asks the browser to authenticate. The browser asks the authenticator to use the relevant credential.&lt;/p&gt;

&lt;p&gt;The authenticator may then say: "Before I use this private key, prove that the actual device user is present."&lt;/p&gt;

&lt;p&gt;Windows Hello might ask for your fingerprint. An iPhone might ask for Face ID. A security key might require you to physically touch it.&lt;/p&gt;

&lt;p&gt;Once that local check succeeds, the authenticator is allowed to use the private key to produce a cryptographic signature.&lt;/p&gt;

&lt;p&gt;The website receives the signature.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; receive your fingerprint image, Face ID data, Windows PIN, or other biometric information.&lt;/p&gt;

&lt;p&gt;That verification remains local to the authenticator and operating system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly is stored inside a passkey?
&lt;/h2&gt;

&lt;p&gt;A passkey is not just "a private key with a website name attached to it."&lt;/p&gt;

&lt;p&gt;Conceptually, a credential is associated with several pieces of information that allow the browser, authenticator, and server to identify and use it correctly.&lt;/p&gt;

&lt;p&gt;The exact internal representation depends on the authenticator, but from the application's point of view, some of the most important pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential ID&lt;/strong&gt; — a unique identifier for this specific credential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private key&lt;/strong&gt; — controlled by the authenticator and used to sign authentication data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public key&lt;/strong&gt; — stored by your server and used to verify those signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relying Party ID&lt;/strong&gt; — the website or domain scope the credential belongs to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User handle&lt;/strong&gt; — an application-defined stable identifier that can associate the credential with a user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverability information&lt;/strong&gt; — for credentials that can be presented before the user enters an email or username.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticator-specific metadata&lt;/strong&gt; — depending on the implementation and platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your server does not need to understand how the authenticator internally stores the private key.&lt;/p&gt;

&lt;p&gt;That is deliberately abstracted away.&lt;/p&gt;

&lt;p&gt;Your application mostly cares about the credential ID, public key, user relationship, and the information required to verify future authentication responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server normally stores for a passkey
&lt;/h2&gt;

&lt;p&gt;When a passkey is registered successfully, you normally create a record in your own database representing that credential.&lt;/p&gt;

&lt;p&gt;A simplified database model might contain values such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User ID&lt;/strong&gt; — which account owns the passkey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential ID&lt;/strong&gt; — the identifier returned by WebAuthn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public key&lt;/strong&gt; — used to verify future signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature counter&lt;/strong&gt; — when provided and relevant for the authenticator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport information&lt;/strong&gt; — hints about how the authenticator can be reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device or credential name&lt;/strong&gt; — often something you add yourself for the user's convenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Created date&lt;/strong&gt; — useful for account management and security history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last-used date&lt;/strong&gt; — useful for displaying credentials and security monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup or authenticator information&lt;/strong&gt; — depending on what your WebAuthn library exposes and what you actually need.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of this information comes directly from WebAuthn. Some of it is metadata your application adds on top.&lt;/p&gt;

&lt;p&gt;For example, WebAuthn does not magically know that you want to show the user a settings page saying:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;MacBook Pro — added August 28 — last used two hours ago.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You build that product layer yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb13uo8eha1rfi007rmh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb13uo8eha1rfi007rmh4.png" alt="Passkey in Settings" width="799" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might ask the user to name a passkey when they create it, or automatically give it a generic label and allow them to rename it later.&lt;/p&gt;

&lt;p&gt;That metadata is not part of the cryptographic secret. It simply makes credential management understandable to humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credential IDs are important
&lt;/h2&gt;

&lt;p&gt;The credential ID deserves more attention because it appears throughout a WebAuthn implementation.&lt;/p&gt;

&lt;p&gt;Imagine a user has three passkeys registered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One on their laptop.&lt;/li&gt;
&lt;li&gt;One in a password manager.&lt;/li&gt;
&lt;li&gt;One on a physical security key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three belong to the same user, but each is a separate credential with its own credential ID and key material.&lt;/p&gt;

&lt;p&gt;When an authentication response comes back, the credential ID tells your backend which stored public key should be used to verify the signature.&lt;/p&gt;

&lt;p&gt;Your server can effectively say:&lt;/p&gt;

&lt;p&gt;"This response says it came from credential &lt;code&gt;ABC123&lt;/code&gt;. Let me find credential &lt;code&gt;ABC123&lt;/code&gt;, get its public key, and verify the signature."&lt;/p&gt;

&lt;p&gt;If the signature verifies and the rest of the WebAuthn checks pass, the server can then look at the owner of that credential and know which user is signing in.&lt;/p&gt;

&lt;p&gt;This is also why users can have multiple passkeys attached to one account without your application becoming confused about which one was used.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an authenticator?
&lt;/h2&gt;

&lt;p&gt;The word &lt;em&gt;authenticator&lt;/em&gt; sounds more complicated than it really is.&lt;/p&gt;

&lt;p&gt;An authenticator is basically the trusted component responsible for creating and using WebAuthn credentials.&lt;/p&gt;

&lt;p&gt;It can usually perform operations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new credential and key pair.&lt;/li&gt;
&lt;li&gt;Store or control access to that credential.&lt;/li&gt;
&lt;li&gt;Ask the user for verification when required.&lt;/li&gt;
&lt;li&gt;Sign authentication data with the credential's private key.&lt;/li&gt;
&lt;li&gt;Return the credential ID and signed response to the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Authenticators are often divided into two broad categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform authenticators
&lt;/h3&gt;

&lt;p&gt;A platform authenticator is integrated into the device you are already using.&lt;/p&gt;

&lt;p&gt;Windows Hello is a good example.&lt;/p&gt;

&lt;p&gt;You are on your laptop, you create a passkey, and later you authenticate using the laptop's fingerprint reader, face recognition, or PIN.&lt;/p&gt;

&lt;p&gt;The user does not have to plug anything in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roaming authenticators
&lt;/h3&gt;

&lt;p&gt;A roaming authenticator is separate from the device running the browser.&lt;/p&gt;

&lt;p&gt;A physical security key is the classic example.&lt;/p&gt;

&lt;p&gt;You might create a credential on a USB or NFC security key and then use that key across multiple computers.&lt;/p&gt;

&lt;p&gt;Modern passkey ecosystems blur these categories a little because phones can also act as authenticators for nearby computers, and passkey providers may synchronize credentials across multiple devices.&lt;/p&gt;

&lt;p&gt;But the mental model remains useful: the authenticator is the thing that controls the credential and performs the cryptographic operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the browser actually do?
&lt;/h2&gt;

&lt;p&gt;The browser is doing much more than displaying a nice fingerprint popup.&lt;/p&gt;

&lt;p&gt;When your frontend calls WebAuthn, the browser participates in enforcing some of the most important security boundaries in the protocol.&lt;/p&gt;

&lt;p&gt;It knows which origin your JavaScript is running on.&lt;/p&gt;

&lt;p&gt;It knows which relying party the website is requesting a credential for.&lt;/p&gt;

&lt;p&gt;It communicates with available authenticators through the operating system and supported authenticator protocols.&lt;/p&gt;

&lt;p&gt;It also packages WebAuthn data in a standardized structure that your backend can later verify.&lt;/p&gt;

&lt;p&gt;This is why WebAuthn is fundamentally different from building a custom JavaScript authentication protocol yourself.&lt;/p&gt;

&lt;p&gt;Your application cannot simply tell the authenticator:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Pretend I am google.com and give me Google's credential."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The browser and authenticator participate in checking whether the relying-party request matches the current web context.&lt;/p&gt;

&lt;h2&gt;
  
  
  How passkey registration works in detail
&lt;/h2&gt;

&lt;p&gt;There are two major WebAuthn ceremonies: registration and authentication.&lt;/p&gt;

&lt;p&gt;Registration is where a new credential is created.&lt;/p&gt;

&lt;p&gt;The process starts on the backend, not inside the authenticator.&lt;/p&gt;

&lt;p&gt;Your backend generates a random challenge and creates a set of registration options. Those options describe things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The challenge.&lt;/li&gt;
&lt;li&gt;The relying party.&lt;/li&gt;
&lt;li&gt;The user identifier.&lt;/li&gt;
&lt;li&gt;The user's display information.&lt;/li&gt;
&lt;li&gt;Whether user verification is required.&lt;/li&gt;
&lt;li&gt;Whether you prefer a discoverable credential.&lt;/li&gt;
&lt;li&gt;Credentials that should be excluded because they already exist.&lt;/li&gt;
&lt;li&gt;Cryptographic algorithms the server accepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your frontend receives those options and passes them to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant U as User
    participant B as Browser
    participant S as Application server
    participant A as Authenticator
    participant D as Database

    U-&amp;gt;&amp;gt;B: Choose "Create a passkey"
    B-&amp;gt;&amp;gt;S: POST /passkeys/register/options
    S-&amp;gt;&amp;gt;S: Generate random challenge
    S-&amp;gt;&amp;gt;S: Set RP ID, origin, user ID, and user verification policy
    S--&amp;gt;&amp;gt;B: Creation options and challenge cookie
    B-&amp;gt;&amp;gt;A: navigator.credentials.create(options)
    U-&amp;gt;&amp;gt;A: Unlock with fingerprint, face, PIN, or security key
    A-&amp;gt;&amp;gt;A: Generate key pair and retain private key
    A--&amp;gt;&amp;gt;B: Return credential ID and public key
    B-&amp;gt;&amp;gt;S: POST /passkeys/register/verify(response)
    S-&amp;gt;&amp;gt;S: Read and consume challenge
    S-&amp;gt;&amp;gt;S: Verify origin, RP ID, challenge, and user verification
    S-&amp;gt;&amp;gt;D: Store credential ID, public key, counter, and metadata
    S--&amp;gt;&amp;gt;B: Registration succeeded&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once the browser receives those options, it invokes an authenticator.&lt;/p&gt;

&lt;p&gt;The authenticator may ask the user for local verification.&lt;/p&gt;

&lt;p&gt;Then it creates the credential and generates its key pair.&lt;/p&gt;

&lt;p&gt;The private key remains under the authenticator's control.&lt;/p&gt;

&lt;p&gt;The browser receives a registration response containing information that allows your server to verify what just happened.&lt;/p&gt;

&lt;p&gt;This registration response is commonly referred to as an &lt;strong&gt;attestation response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The server then verifies several things before trusting it.&lt;/p&gt;

&lt;p&gt;Among them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was this response created for the challenge we just issued?&lt;/li&gt;
&lt;li&gt;Has that challenge already been used?&lt;/li&gt;
&lt;li&gt;Does the origin match the website we expect?&lt;/li&gt;
&lt;li&gt;Does the relying-party information match?&lt;/li&gt;
&lt;li&gt;Was user verification performed if we required it?&lt;/li&gt;
&lt;li&gt;Can the credential data be parsed and cryptographically validated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after those checks succeed do you save the credential.&lt;/p&gt;

&lt;p&gt;This is why the registration response should never simply be accepted because "the browser sent it."&lt;/p&gt;

&lt;p&gt;The browser is part of the protocol, but your backend still treats anything arriving over HTTP as untrusted input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attestation does not mean your server receives the private key
&lt;/h2&gt;

&lt;p&gt;The name &lt;em&gt;attestation&lt;/em&gt; can make the registration response sound as if the authenticator is handing your backend everything about itself.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;The important result for your application is that you receive the newly created credential's public information and enough signed or structured data to validate the registration ceremony.&lt;/p&gt;

&lt;p&gt;The private key does not come along for the ride.&lt;/p&gt;

&lt;p&gt;Your WebAuthn library typically parses the registration response and gives you the pieces you actually need to store, such as the credential ID, public key, and counter.&lt;/p&gt;

&lt;p&gt;Depending on your authenticator policy and implementation, attestation can contain additional information about the authenticator. Many consumer-facing applications do not need to build their product around identifying the exact hardware model of every authenticator.&lt;/p&gt;

&lt;p&gt;In most normal web applications, your real goal is simpler:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Did a valid authenticator create a valid credential for the correct user and the correct website in response to the challenge I generated?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How passkey authentication works
&lt;/h2&gt;

&lt;p&gt;Authentication is the second ceremony.&lt;/p&gt;

&lt;p&gt;This time, no new key pair needs to be generated.&lt;/p&gt;

&lt;p&gt;Your server wants the authenticator to prove that it controls the private key corresponding to one of the public keys you already have stored.&lt;/p&gt;

&lt;p&gt;Again, the backend begins by generating a fresh random challenge.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant U as User
    participant B as Browser
    participant S as Application server
    participant A as Authenticator
    participant D as Database

    U-&amp;gt;&amp;gt;B: Choose "Sign in with a passkey"
    B-&amp;gt;&amp;gt;S: POST /passkeys/authenticate/options
    S-&amp;gt;&amp;gt;S: Generate fresh challenge
    S--&amp;gt;&amp;gt;B: Request options + challenge cookie
    B-&amp;gt;&amp;gt;A: navigator.credentials.get(options)
    U-&amp;gt;&amp;gt;A: Unlock authenticator
    A-&amp;gt;&amp;gt;A: Sign challenge + origin + RP data with private key
    A--&amp;gt;&amp;gt;B: Assertion: credential ID + signature
    B-&amp;gt;&amp;gt;S: POST /passkeys/authenticate/verify(assertion)
    S-&amp;gt;&amp;gt;D: Find credential by credential ID
    S-&amp;gt;&amp;gt;S: Verify signature with stored public key
    S-&amp;gt;&amp;gt;S: Check challenge, origin, RP ID, user verification, counter
    S-&amp;gt;&amp;gt;S: Create authenticated session
    S--&amp;gt;&amp;gt;B: Session cookies / tokens&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The browser asks the authenticator for a matching credential.&lt;/p&gt;

&lt;p&gt;The authenticator may ask the user to verify themselves locally.&lt;/p&gt;

&lt;p&gt;Once authorized, the authenticator signs data related to the authentication ceremony using the private key.&lt;/p&gt;

&lt;p&gt;The result sent back to the server is generally called an &lt;strong&gt;assertion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The assertion includes the credential ID, which allows your backend to identify the correct stored credential.&lt;/p&gt;

&lt;p&gt;Your backend finds that database record, loads its public key, and asks the WebAuthn verification library to verify the signature and the surrounding protocol data.&lt;/p&gt;

&lt;p&gt;If everything checks out, you now know that the party producing the response controls the private key corresponding to the public key you stored during registration.&lt;/p&gt;

&lt;p&gt;Notice what did &lt;strong&gt;not&lt;/strong&gt; happen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The private key was never sent to your backend.&lt;/li&gt;
&lt;li&gt;The user did not send a reusable password.&lt;/li&gt;
&lt;li&gt;The fingerprint was never sent to your backend.&lt;/li&gt;
&lt;li&gt;The server did not decrypt anything using the public key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server simply verified a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the challenge matters
&lt;/h2&gt;

&lt;p&gt;The challenge is one of those details that can look like boring protocol plumbing until you realize what attack it prevents.&lt;/p&gt;

&lt;p&gt;Suppose an attacker somehow records a valid authentication response from yesterday.&lt;/p&gt;

&lt;p&gt;Without a fresh challenge, they could potentially send that exact response to your server again.&lt;/p&gt;

&lt;p&gt;A challenge prevents that.&lt;/p&gt;

&lt;p&gt;Your server generates a random value specifically for the current authentication attempt. The signed response is tied to that value.&lt;/p&gt;

&lt;p&gt;If someone replays an old response, it contains an old challenge and verification fails.&lt;/p&gt;

&lt;p&gt;This is why challenges should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cryptographically random.&lt;/li&gt;
&lt;li&gt;Short-lived.&lt;/li&gt;
&lt;li&gt;Associated with the correct ceremony or session.&lt;/li&gt;
&lt;li&gt;Consumed after successful or attempted use according to your design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should not generate a permanent "challenge" once for every user and keep reusing it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why passkeys are resistant to phishing
&lt;/h2&gt;

&lt;p&gt;This is one of the parts I found particularly interesting.&lt;/p&gt;

&lt;p&gt;A WebAuthn credential is scoped to something called a &lt;strong&gt;relying party ID&lt;/strong&gt;, or RP ID, which is normally related to the website's domain.&lt;/p&gt;

&lt;p&gt;A passkey created for &lt;code&gt;example.com&lt;/code&gt; cannot simply be used by &lt;code&gt;evil-example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The browser and authenticator participate in enforcing this relationship.&lt;/p&gt;

&lt;p&gt;So imagine an attacker builds a perfect clone of your login screen.&lt;/p&gt;

&lt;p&gt;With a traditional password, the user can accidentally type their real password into the fake website. The attacker now possesses something reusable.&lt;/p&gt;

&lt;p&gt;With WebAuthn, the fake website does not get to simply request a valid credential for the real domain.&lt;/p&gt;

&lt;p&gt;The credential is tied to the relying party it was created for.&lt;/p&gt;

&lt;p&gt;There is no equivalent of "just type your passkey into this phishing form."&lt;/p&gt;

&lt;p&gt;This is one of the strongest conceptual improvements passkeys bring to everyday authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the RP ID?
&lt;/h2&gt;

&lt;p&gt;The RP ID is another detail that becomes extremely important when you move from local development to production.&lt;/p&gt;

&lt;p&gt;RP stands for &lt;strong&gt;Relying Party&lt;/strong&gt;. In this case, your application is the relying party because it relies on the authenticator to prove the user's identity.&lt;/p&gt;

&lt;p&gt;The RP ID is normally your application's domain or an appropriate registrable domain scope.&lt;/p&gt;

&lt;p&gt;It determines which website context a credential belongs to.&lt;/p&gt;

&lt;p&gt;If you configure it incorrectly, passkeys that worked beautifully on localhost can suddenly fail in production.&lt;/p&gt;

&lt;p&gt;It is especially important when dealing with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subdomains.&lt;/li&gt;
&lt;li&gt;Separate frontend and API hosts.&lt;/li&gt;
&lt;li&gt;Development versus production environments.&lt;/li&gt;
&lt;li&gt;Preview deployments.&lt;/li&gt;
&lt;li&gt;Multiple products sharing authentication infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your expected origin and RP ID should be explicit configuration, not something you casually infer from whatever host header happened to arrive in a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discoverable credentials and username-less login
&lt;/h2&gt;

&lt;p&gt;Another nice part of passkeys is that they can support a completely username-less login experience.&lt;/p&gt;

&lt;p&gt;Traditionally, your server needs to know the user's account first.&lt;/p&gt;

&lt;p&gt;The user enters:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user@example.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Your backend then looks up that user and figures out which authentication methods belong to the account.&lt;/p&gt;

&lt;p&gt;Discoverable credentials can reverse that flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpanel.sadeq.dev%2Fassets%2F71497075-9c29-4864-8029-3b70d3dfad84.avif%3Fwidth%3D1372%26height%3D958" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpanel.sadeq.dev%2Fassets%2F71497075-9c29-4864-8029-3b70d3dfad84.avif%3Fwidth%3D1372%26height%3D958" alt="Usernameless Signin Alphaself Application" width="1372" height="958"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser or authenticator can present credentials that match the current relying party before your server knows which account the user wants.&lt;/p&gt;

&lt;p&gt;The user can select a passkey, authenticate, and the resulting credential tells your backend which account owns it.&lt;/p&gt;

&lt;p&gt;This is how you can build a login screen where the user does not type an email address at all.&lt;/p&gt;

&lt;p&gt;You can also keep a username-first flow.&lt;/p&gt;

&lt;p&gt;In that version, the user enters an email address first. Your backend finds the credentials associated with that account and sends those credential IDs in &lt;code&gt;allowCredentials&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The browser then knows which credentials are acceptable for that specific authentication attempt.&lt;/p&gt;

&lt;p&gt;Both approaches are valid.&lt;/p&gt;

&lt;p&gt;Personally, I find the discoverable flow much more interesting because that is where passkeys stop feeling like "passwords but with a fingerprint" and start changing the actual login experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synced passkeys versus device-bound credentials
&lt;/h2&gt;

&lt;p&gt;One phrase you will often hear is:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The private key never leaves your device.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is a useful beginner explanation, but with modern synced passkeys it can also become misleading if taken too literally.&lt;/p&gt;

&lt;p&gt;Some passkey ecosystems can securely synchronize a user's credentials across devices through a credential provider.&lt;/p&gt;

&lt;p&gt;That is how a passkey created on one device may later appear on another device belonging to the same user.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Authentication/Passkeys" rel="noopener noreferrer"&gt;MDN passkeys guide&lt;/a&gt; is a useful reference if you want to dig further into synced passkeys, authentication flows, and credential management.&lt;/p&gt;

&lt;p&gt;From your application's point of view, the important guarantee remains the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your application never receives the private key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You do not export it from WebAuthn, store it in your database, or send it back to the browser during authentication.&lt;/p&gt;

&lt;p&gt;How a passkey provider protects or synchronizes its credential material is part of that provider's security architecture.&lt;/p&gt;

&lt;p&gt;Other credentials may be device-bound.&lt;/p&gt;

&lt;p&gt;For example, a hardware security key may contain a credential that remains associated with that physical authenticator.&lt;/p&gt;

&lt;p&gt;This distinction matters when thinking about recovery and cross-device usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the frontend do?
&lt;/h2&gt;

&lt;p&gt;One mistake I initially expected to make was thinking that implementing passkeys would involve writing a lot of cryptography in the frontend.&lt;/p&gt;

&lt;p&gt;Thankfully, it should not.&lt;/p&gt;

&lt;p&gt;The frontend's job is mostly orchestration.&lt;/p&gt;

&lt;p&gt;It asks your backend for WebAuthn options, passes those options into a WebAuthn-capable browser API or client library, and sends the resulting response back to the server.&lt;/p&gt;

&lt;p&gt;The browser and authenticator handle the sensitive cryptographic operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerPasskey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deviceName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/passkeys/register/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startRegistration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;optionsJSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/passkeys/register/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signInWithPasskey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/passkeys/authenticate/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startAuthentication&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;optionsJSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/passkeys/authenticate/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend should &lt;strong&gt;not&lt;/strong&gt; be trusted to decide whether authentication succeeded.&lt;/p&gt;

&lt;p&gt;It should not say:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The browser returned something, therefore the user is authenticated.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The backend still has to verify the cryptographic response.&lt;/p&gt;

&lt;p&gt;The frontend also needs to handle the less glamorous UX cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user cancels the passkey prompt.&lt;/li&gt;
&lt;li&gt;No compatible authenticator is available.&lt;/li&gt;
&lt;li&gt;The ceremony times out.&lt;/li&gt;
&lt;li&gt;The challenge expires.&lt;/li&gt;
&lt;li&gt;The browser does not support the required flow.&lt;/li&gt;
&lt;li&gt;The requested credential cannot be found.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cancelled fingerprint dialog is not necessarily a server error.&lt;/p&gt;

&lt;p&gt;Your UI should treat user cancellation as a normal outcome rather than showing some terrifying red error banner every time someone changes their mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the backend do?
&lt;/h2&gt;

&lt;p&gt;The backend is where most of the security-sensitive verification happens.&lt;/p&gt;

&lt;p&gt;I would strongly avoid implementing the WebAuthn parsing and cryptographic verification yourself.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of protocol where using a mature, maintained library is much better than trying to be clever.&lt;/p&gt;

&lt;p&gt;Your backend is responsible for things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating secure registration and authentication challenges.&lt;/li&gt;
&lt;li&gt;Creating WebAuthn options.&lt;/li&gt;
&lt;li&gt;Associating registration with the correct authenticated user.&lt;/li&gt;
&lt;li&gt;Verifying registration responses.&lt;/li&gt;
&lt;li&gt;Saving credential public information.&lt;/li&gt;
&lt;li&gt;Looking up credentials during authentication.&lt;/li&gt;
&lt;li&gt;Verifying authentication assertions.&lt;/li&gt;
&lt;li&gt;Updating relevant credential state.&lt;/li&gt;
&lt;li&gt;Creating the final application session.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Registration options&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;rpID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rpName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;excludeCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existingCredentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authenticatorSelection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;residentKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferred&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveShortLivedChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;

&lt;span class="c1"&gt;// Registration verification&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;consumeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyRegistrationResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;passkeyOrigin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedRPID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rpID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requireUserVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Authentication verification&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;consumeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByCredentialId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyAuthenticationResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;passkeyOrigin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedRPID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rpID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requireUserVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateCounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCounter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;issueSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code itself doesn't look particularly scary.&lt;/p&gt;

&lt;p&gt;The dangerous part is getting the assumptions around it wrong.&lt;/p&gt;

&lt;p&gt;In production, you need to care about exact origin and RP ID configuration, challenge lifetime, replay protection, authorization, account state, secure cookies, rate limiting, credential-management permissions, and recovery.&lt;/p&gt;

&lt;p&gt;Your authentication endpoint should not create a session until the WebAuthn response has been fully verified.&lt;/p&gt;

&lt;p&gt;Your credential deletion endpoint should not remove a passkey just because someone knows its credential ID.&lt;/p&gt;

&lt;p&gt;Your registration endpoint should not allow someone to attach a passkey to another user's account.&lt;/p&gt;

&lt;p&gt;These things sound obvious when written down, but most authentication vulnerabilities live in exactly these surrounding application rules rather than in the cryptography itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about signature counters?
&lt;/h2&gt;

&lt;p&gt;You will often see a &lt;code&gt;counter&lt;/code&gt; stored alongside a WebAuthn credential.&lt;/p&gt;

&lt;p&gt;Historically, authenticators could increment a signature counter whenever the credential was used. The server could compare the new value against the previous one.&lt;/p&gt;

&lt;p&gt;If the server saw something strange, such as a counter unexpectedly moving backward or being reused, it could potentially indicate that a credential had been cloned.&lt;/p&gt;

&lt;p&gt;In practice, you should not build your entire passkey security model around signature counters.&lt;/p&gt;

&lt;p&gt;Different authenticators behave differently, and some modern authenticators or synced passkeys may not give you a useful monotonically increasing counter in the way older explanations of WebAuthn sometimes imply.&lt;/p&gt;

&lt;p&gt;The correct approach is generally to let your WebAuthn library handle the protocol behavior and store the counter information it expects, rather than inventing your own cloned-authenticator detection logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata is part of the product experience, not just the protocol
&lt;/h2&gt;

&lt;p&gt;The cryptographic side of passkeys gets most of the attention, but there is another layer that matters a lot once you actually ship this to users: credential management.&lt;/p&gt;

&lt;p&gt;If someone has four passkeys, they need some way to understand what they are looking at.&lt;/p&gt;

&lt;p&gt;A settings page might show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows laptop&lt;/strong&gt; — added August 21 — last used today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1Password&lt;/strong&gt; — added August 12 — last used August 25.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security key&lt;/strong&gt; — added July 3 — last used August 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebAuthn does not automatically give you a perfect human-readable name for every credential.&lt;/p&gt;

&lt;p&gt;Your application may need to create that layer.&lt;/p&gt;

&lt;p&gt;For example, when a user creates a passkey, you might ask:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What should we call this passkey?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Or you might create a generic name and let them edit it later.&lt;/p&gt;

&lt;p&gt;You can also store application metadata such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the credential was registered.&lt;/li&gt;
&lt;li&gt;When it was last successfully used.&lt;/li&gt;
&lt;li&gt;A user-defined name.&lt;/li&gt;
&lt;li&gt;Whether your application considers it a recovery credential.&lt;/li&gt;
&lt;li&gt;Potentially browser or authenticator information where appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should be careful not to turn this into invasive device fingerprinting just because you can collect metadata.&lt;/p&gt;

&lt;p&gt;Store what genuinely improves security or usability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credential management deserves its own security rules
&lt;/h2&gt;

&lt;p&gt;Once passkeys exist, users need to be able to manage them.&lt;/p&gt;

&lt;p&gt;Usually that means allowing them to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;See registered passkeys.&lt;/li&gt;
&lt;li&gt;Add another passkey.&lt;/li&gt;
&lt;li&gt;Rename a passkey.&lt;/li&gt;
&lt;li&gt;Remove a lost or old passkey.&lt;/li&gt;
&lt;li&gt;Potentially see when each passkey was last used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These actions are security-sensitive.&lt;/p&gt;

&lt;p&gt;Imagine an attacker manages to hijack a user's browser session for five minutes.&lt;/p&gt;

&lt;p&gt;If they can silently add their own passkey, they may turn a temporary session compromise into persistent access.&lt;/p&gt;

&lt;p&gt;That means adding or deleting authentication methods may deserve stronger protections than editing a profile photo.&lt;/p&gt;

&lt;p&gt;Depending on the value of the account, you might require recent authentication, another passkey confirmation, a notification to the user, or additional recovery controls when credentials change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery is part of passkey authentication
&lt;/h2&gt;

&lt;p&gt;Passkeys solve a lot of password problems, but they do not remove the concept of account recovery.&lt;/p&gt;

&lt;p&gt;Users lose phones.&lt;/p&gt;

&lt;p&gt;Laptops die.&lt;/p&gt;

&lt;p&gt;People forget device PINs.&lt;/p&gt;

&lt;p&gt;Security keys disappear.&lt;/p&gt;

&lt;p&gt;Cloud accounts get locked.&lt;/p&gt;

&lt;p&gt;A serious passkey implementation needs to answer:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens now?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Synced passkeys make this easier because a user's credentials may become available again through their platform account on a new device.&lt;/p&gt;

&lt;p&gt;But you cannot assume that every user will always have access to a synced credential.&lt;/p&gt;

&lt;p&gt;One useful strategy is allowing users to register multiple passkeys.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laptop passkey.&lt;/li&gt;
&lt;li&gt;Phone or synced-provider passkey.&lt;/li&gt;
&lt;li&gt;Hardware security key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then losing one authenticator does not necessarily mean losing the entire account.&lt;/p&gt;

&lt;p&gt;You can still provide another recovery method, but remember something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your account is only as secure as its easiest recovery path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the user has incredibly strong phishing-resistant passkey authentication but an attacker can click &lt;em&gt;Forgot access?&lt;/em&gt; and take over the account through a weak recovery email flow, your passkey implementation has not achieved nearly as much as it appears to have achieved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can passkeys work on another computer?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;This confused me initially because "the private key is on your device" makes it sound like a passkey should be permanently trapped inside one machine.&lt;/p&gt;

&lt;p&gt;There are several ways cross-device authentication can work.&lt;/p&gt;

&lt;p&gt;A synced passkey may already be available through the user's credential provider on another device.&lt;/p&gt;

&lt;p&gt;A phone can also be used as an authenticator for a nearby computer.&lt;/p&gt;

&lt;p&gt;You may have seen this flow already: the computer displays a QR code, you scan it with your phone, confirm that the devices are nearby, authenticate on your phone, and the computer's browser completes the login.&lt;/p&gt;

&lt;p&gt;A hardware security key can simply be moved between supported computers.&lt;/p&gt;

&lt;p&gt;The exact UX depends on the browser, operating system, authenticator, and credential provider, but passkeys are not inherently limited to the device on which they were originally created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passkeys make authentication feel simpler because the complexity moved somewhere better
&lt;/h2&gt;

&lt;p&gt;That is probably my biggest takeaway from learning how passkeys actually work.&lt;/p&gt;

&lt;p&gt;Passwords put a surprising amount of responsibility on the user.&lt;/p&gt;

&lt;p&gt;We ask people to invent secrets, remember them, avoid reusing them, detect phishing websites, use password managers, and sometimes type another six-digit code afterward.&lt;/p&gt;

&lt;p&gt;Then we blame them when one of those steps goes wrong.&lt;/p&gt;

&lt;p&gt;Passkeys move much of that responsibility into browsers, authenticators, operating systems, credential providers, and well-defined cryptographic protocols.&lt;/p&gt;

&lt;p&gt;The user sees:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Click. Fingerprint. You're in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Underneath that, however, quite a lot has happened.&lt;/p&gt;

&lt;p&gt;Your backend generated a fresh challenge. The browser checked the relying-party context. The authenticator found the correct credential. The user locally unlocked that authenticator. The private key signed the authentication data. The server found the corresponding public key and verified the signature. Only then was the authenticated session created.&lt;/p&gt;

&lt;p&gt;And throughout that whole process, the private key never needed to be sent to your application.&lt;/p&gt;

&lt;p&gt;That is the part that makes the system so interesting.&lt;/p&gt;

&lt;p&gt;The experience became simpler without making the authentication model simpler or weaker underneath.&lt;/p&gt;

&lt;p&gt;In the next article, I'll take this into actual code and build the registration, authentication, credential-management, and recovery flow using Vue, Nuxt, and a WebAuthn library on the backend.&lt;/p&gt;

</description>
      <category>passkeys</category>
      <category>authentication</category>
      <category>webautn</category>
    </item>
    <item>
      <title>Static File Caching in Nuxt: An Easy and Practical Strategy</title>
      <dc:creator>Sadeq Sheikhi</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:29:02 +0000</pubDate>
      <link>https://dev.to/sadegh_shaikhi_0549a5c17f/static-file-caching-in-nuxt-an-easy-and-practical-strategy-57oo</link>
      <guid>https://dev.to/sadegh_shaikhi_0549a5c17f/static-file-caching-in-nuxt-an-easy-and-practical-strategy-57oo</guid>
      <description>&lt;p&gt;Lighthouse kept warning me about inefficient cache lifetimes, even though I had already added caching for my static files.&lt;/p&gt;

&lt;p&gt;The missing piece was &lt;strong&gt;Nuxt Image and its generated &lt;code&gt;/_ipx&lt;/code&gt; URLs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I’ll share the simple caching setup I use for Nuxt build files, public assets, and optimized images without risking stale content after deployment.&lt;/p&gt;

&lt;p&gt;The basic rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cache files aggressively when changing the file also changes its URL. Be more careful when the same URL can serve different content later.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You have probably seen the same Lighthouse warning I have:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use efficient cache lifetimes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Browser caching for static files is usually straightforward. You add a &lt;code&gt;Cache-Control&lt;/code&gt; header, choose a reasonable lifetime, and the browser avoids downloading the same files again on every visit.&lt;/p&gt;

&lt;p&gt;However, in a Nuxt application, not every static-looking file should use the same caching policy.&lt;/p&gt;

&lt;p&gt;Nuxt build files are automatically versioned. Files inside &lt;code&gt;public/&lt;/code&gt; usually are not. Nuxt Image also creates transformed image URLs under &lt;code&gt;/_ipx&lt;/code&gt;, which need their own cache rule.&lt;/p&gt;

&lt;p&gt;In this post, I’ll go through the setup I use, including the Nuxt Image rule that was missing during my latest Lighthouse audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simple caching rule
&lt;/h2&gt;

&lt;p&gt;The most important question is not whether a file is an image, font, or JavaScript file.&lt;/p&gt;

&lt;p&gt;The important question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will the URL change when the file changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the answer is yes, you can safely cache the file for a very long time.&lt;/p&gt;

&lt;p&gt;When the answer is no, you should use a shorter cache lifetime. Otherwise, visitors may continue seeing an old version after you deploy an update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the cache directives mean
&lt;/h2&gt;

&lt;p&gt;Here are the main directives used in this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public&lt;/code&gt; allows browsers and shared caches such as CDNs to store the response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-age&lt;/code&gt; controls how long the browser considers the file fresh.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s-maxage&lt;/code&gt; controls how long shared caches such as Cloudflare consider it fresh.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;immutable&lt;/code&gt; tells the browser that the file is not expected to change while that URL exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important one here is &lt;code&gt;immutable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You should only use it when changing the file also results in a new URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three main asset types in Nuxt
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Nuxt build files: &lt;code&gt;/_nuxt/**&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Nuxt and Vite generate filenames that contain a content hash.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_nuxt/entry.Bx3k9Qp2.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the file changes, its hash changes too.&lt;/p&gt;

&lt;p&gt;That means the URL changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_nuxt/entry.Bx3k9Qp2.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might become something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_nuxt/entry.Kp8wZ2dA.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;after another deployment.&lt;/p&gt;

&lt;p&gt;This makes Nuxt build files perfect candidates for aggressive caching.&lt;/p&gt;

&lt;p&gt;You can cache them for a year and use &lt;code&gt;immutable&lt;/code&gt; because the next deployment will reference a completely different file when its contents change.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Files inside &lt;code&gt;public/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Files in Nuxt's &lt;code&gt;public/&lt;/code&gt; directory work differently.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/img/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is served as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that its URL does not automatically change when the file changes.&lt;/p&gt;

&lt;p&gt;You could replace &lt;code&gt;logo.png&lt;/code&gt; during your next deployment while keeping exactly the same URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that file was cached for a year with &lt;code&gt;immutable&lt;/code&gt;, some users could continue seeing the old logo.&lt;/p&gt;

&lt;p&gt;For these files, I normally do one of two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give them a reasonable cache lifetime, such as several days or weeks.&lt;/li&gt;
&lt;li&gt;Version the filename when I need aggressive caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logo.v2.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logo.2026-08.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the filename itself changes whenever the content changes, long-term caching becomes much safer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Nuxt Image files: &lt;code&gt;/_ipx/**&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the part that caused my Lighthouse warning.&lt;/p&gt;

&lt;p&gt;When you use Nuxt Image with the local IPX provider, the browser often doesn't request your original image directly.&lt;/p&gt;

&lt;p&gt;Instead, Nuxt generates a transformed image URL that can look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/q_80&amp;amp;s_640x360/projects/sharmarket.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That URL contains information about the requested transformation, such as image quality and dimensions.&lt;/p&gt;

&lt;p&gt;So you may have an original image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/project.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the browser actually loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/q_80&amp;amp;s_640x360/img/project.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are two completely different HTTP requests.&lt;/p&gt;

&lt;p&gt;And therefore, they can have completely different caching headers.&lt;/p&gt;

&lt;p&gt;This was exactly what I had missed.&lt;/p&gt;

&lt;p&gt;I already had caching configured for my normal image directory, but Lighthouse was complaining about the generated &lt;code&gt;/_ipx&lt;/code&gt; request.&lt;/p&gt;

&lt;p&gt;Adding a rule for &lt;code&gt;/img/**&lt;/code&gt; does not automatically cache &lt;code&gt;/_ipx/**&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is another important detail here.&lt;/p&gt;

&lt;p&gt;I don't treat IPX URLs as completely immutable by default.&lt;/p&gt;

&lt;p&gt;Imagine that this source image changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/projects/sharmarket.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but you keep the same filename.&lt;/p&gt;

&lt;p&gt;The corresponding IPX URL may also remain the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/q_80&amp;amp;s_640x360/projects/sharmarket.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though the image being returned has changed.&lt;/p&gt;

&lt;p&gt;Because of that, I give IPX responses a relatively long cache lifetime, but I don't add &lt;code&gt;immutable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nuxt route rules
&lt;/h2&gt;

&lt;p&gt;Here is the setup I currently use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;routeRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_nuxt/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=31536000,s-maxage=31536000,immutable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_ipx/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=2592000,s-maxage=2592000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/img/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=864000,s-maxage=864000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fonts/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=5184000,s-maxage=5184000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/js/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=2592000,s-maxage=2592000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values are roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/_nuxt/**&lt;/code&gt; → one year&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/_ipx/**&lt;/code&gt; → 30 days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/img/**&lt;/code&gt; → 10 days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fonts/**&lt;/code&gt; → 60 days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/js/**&lt;/code&gt; → 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not magic numbers.&lt;/p&gt;

&lt;p&gt;They are simply reasonable defaults based on how likely each type of file is to change without its URL changing.&lt;/p&gt;

&lt;p&gt;Your application may need different values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the IPX rule matters
&lt;/h2&gt;

&lt;p&gt;This distinction is easy to overlook.&lt;/p&gt;

&lt;p&gt;Suppose you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/project.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and Nuxt Image generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/q_80&amp;amp;s_640x360/img/project.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A route rule like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/img/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=864000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only applies to the first URL.&lt;/p&gt;

&lt;p&gt;It does not automatically apply to the IPX response.&lt;/p&gt;

&lt;p&gt;Your browser, CDN, Lighthouse, and server all see those as different URLs.&lt;/p&gt;

&lt;p&gt;So if Lighthouse complains about an image cache lifetime in a Nuxt application, inspect the actual request URL.&lt;/p&gt;

&lt;p&gt;If it begins with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then changing your &lt;code&gt;/img/**&lt;/code&gt; cache rules isn't going to fix it.&lt;/p&gt;

&lt;p&gt;You need a separate &lt;code&gt;/_ipx/**&lt;/code&gt; rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not use &lt;code&gt;immutable&lt;/code&gt; everywhere
&lt;/h2&gt;

&lt;p&gt;It can be tempting to do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache-Control: public,max-age=31536000,immutable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for every static asset.&lt;/p&gt;

&lt;p&gt;Lighthouse will probably be happier.&lt;/p&gt;

&lt;p&gt;Your future self might not be.&lt;/p&gt;

&lt;p&gt;Imagine you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and you cache it for one year with &lt;code&gt;immutable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One week later, you deploy a new logo under exactly the same URL.&lt;/p&gt;

&lt;p&gt;Some browsers may keep using the old file because you explicitly told them that this URL would not change.&lt;/p&gt;

&lt;p&gt;A safer strategy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use very long caching plus &lt;code&gt;immutable&lt;/code&gt; for hashed files.&lt;/li&gt;
&lt;li&gt;Use it for manually versioned files.&lt;/li&gt;
&lt;li&gt;Use shorter caching for URLs that can serve different content later.&lt;/li&gt;
&lt;li&gt;Change the filename when you need reliable cache invalidation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to give everything a one-year cache because Lighthouse asked nicely.&lt;/p&gt;

&lt;p&gt;The goal is to cache every file &lt;strong&gt;for as long as its URL structure safely allows&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare and CDN caching
&lt;/h2&gt;

&lt;p&gt;There is another layer involved if you're using Cloudflare or another CDN.&lt;/p&gt;

&lt;p&gt;Your Nuxt route rules define headers at your application level.&lt;/p&gt;

&lt;p&gt;But Cloudflare sits between your application and your users.&lt;/p&gt;

&lt;p&gt;Depending on your configuration, Cloudflare may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;respect your origin caching headers,&lt;/li&gt;
&lt;li&gt;apply its own caching rules,&lt;/li&gt;
&lt;li&gt;decide that a response is not cacheable,&lt;/li&gt;
&lt;li&gt;or override some of your settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So after deploying a caching change, don't just assume it worked.&lt;/p&gt;

&lt;p&gt;Check the actual response.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/_ipx/q_80&amp;amp;s_640x360/projects/sharmarket.webp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache-control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're using Cloudflare, also look for headers such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cf-cache-status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you much more than staring at your &lt;code&gt;nuxt.config.ts&lt;/code&gt; and assuming everything between your server and the browser behaves exactly as expected.&lt;/p&gt;

&lt;p&gt;Also check your Cloudflare Cache Rules and Workers if the headers you're receiving don't match what Nuxt is sending.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical default strategy
&lt;/h2&gt;

&lt;p&gt;If you just want a reasonable starting point, this is how I think about it:&lt;/p&gt;

&lt;h3&gt;
  
  
  Hashed Nuxt build files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_nuxt/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache aggressively.&lt;/p&gt;

&lt;p&gt;One year plus &lt;code&gt;immutable&lt;/code&gt; makes sense because changing the content produces a different filename.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioned public files
&lt;/h3&gt;

&lt;p&gt;Files like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/logo.v4.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can also be cached aggressively because you'll create a new URL when you replace them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normal public assets
&lt;/h3&gt;

&lt;p&gt;Files such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should generally use shorter cache lifetimes unless you have a versioning strategy.&lt;/p&gt;

&lt;p&gt;Several days or weeks is often enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt Image / IPX responses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/_ipx/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can benefit significantly from caching, but I avoid marking them immutable unless the underlying image paths are also guaranteed to change when the source changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify the setup
&lt;/h2&gt;

&lt;p&gt;After deploying your changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your browser's developer tools.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Network&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Inspect one file under &lt;code&gt;/_nuxt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect an original image from &lt;code&gt;/img&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect an optimized Nuxt Image request under &lt;code&gt;/_ipx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the &lt;code&gt;Cache-Control&lt;/code&gt; response header for each one.&lt;/li&gt;
&lt;li&gt;If you're using Cloudflare, inspect &lt;code&gt;cf-cache-status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run Lighthouse again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is especially useful because the URL Lighthouse reports might not be the URL you expected.&lt;/p&gt;

&lt;p&gt;That was the mistake in my case.&lt;/p&gt;

&lt;p&gt;I was looking at my original images while Lighthouse was actually complaining about their transformed IPX versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is static file caching necessary on Vercel or similar platforms?
&lt;/h3&gt;

&lt;p&gt;Yes, but you may not need to configure every part of it yourself.&lt;/p&gt;

&lt;p&gt;Platforms like Vercel already handle many framework-generated assets efficiently, especially hashed build files.&lt;/p&gt;

&lt;p&gt;That doesn't mean every URL in your application automatically receives the exact cache policy you want.&lt;/p&gt;

&lt;p&gt;Things like public files, custom server routes, and generated image URLs such as &lt;code&gt;/_ipx&lt;/code&gt; can still be worth checking.&lt;/p&gt;

&lt;p&gt;The important thing is to inspect the actual response headers instead of assuming the hosting platform has already handled everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shouldn't Nginx handle this?
&lt;/h3&gt;

&lt;p&gt;It can.&lt;/p&gt;

&lt;p&gt;If Nginx sits in front of your Nuxt application, you can absolutely configure caching headers there.&lt;/p&gt;

&lt;p&gt;For example, you could define different policies for &lt;code&gt;/_nuxt&lt;/code&gt;, &lt;code&gt;/img&lt;/code&gt;, fonts, scripts, and other paths.&lt;/p&gt;

&lt;p&gt;I personally prefer putting these rules inside Nuxt when possible because the caching policy then lives with the application.&lt;/p&gt;

&lt;p&gt;If I move the application to another server or deployment platform, I don't have to remember that an important part of its behavior was hidden inside an Nginx configuration somewhere else.&lt;/p&gt;

&lt;p&gt;But both approaches are valid.&lt;/p&gt;

&lt;h3&gt;
  
  
  I deploy with Coolify. Doesn't Coolify have options for this?
&lt;/h3&gt;

&lt;p&gt;Coolify handles deployment and usually places a reverse proxy such as Traefik in front of your application.&lt;/p&gt;

&lt;p&gt;That infrastructure can be configured to manipulate response headers, but it doesn't automatically know what caching policy makes sense for your application.&lt;/p&gt;

&lt;p&gt;For example, it cannot magically know that your &lt;code&gt;/_nuxt&lt;/code&gt; files are hashed while &lt;code&gt;/img/logo.png&lt;/code&gt; may be replaced later under the same URL.&lt;/p&gt;

&lt;p&gt;That's application-specific knowledge.&lt;/p&gt;

&lt;p&gt;For me, defining the policy in Nuxt is simpler and makes the application more portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if I want a specific file not to be cached?
&lt;/h3&gt;

&lt;p&gt;You can create a more specific route rule.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;routeRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/img/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public,max-age=864000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/img/dynamic-image.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specific route can override the broader one.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;no-store&lt;/code&gt; when you genuinely don't want the response stored.&lt;/p&gt;

&lt;p&gt;If you still want caching but want clients to revalidate the resource before using it, &lt;code&gt;no-cache&lt;/code&gt; has a different meaning and may be more appropriate.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I purge a cached file when necessary?
&lt;/h3&gt;

&lt;p&gt;This depends on where it has been cached.&lt;/p&gt;

&lt;p&gt;If a CDN such as Cloudflare has the file, you can purge it from the CDN.&lt;/p&gt;

&lt;p&gt;If you have another reverse proxy or server-side cache, that may also have its own invalidation mechanism.&lt;/p&gt;

&lt;p&gt;The tricky part is the browser cache.&lt;/p&gt;

&lt;p&gt;You generally cannot remotely force every user's browser to delete a file it has already cached.&lt;/p&gt;

&lt;p&gt;That's why versioned URLs are so useful.&lt;/p&gt;

&lt;p&gt;Instead of trying to make users forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/logo.v2.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the browser sees a completely new URL and requests it normally.&lt;/p&gt;

&lt;p&gt;For important static assets, URL versioning is often a much cleaner invalidation strategy than relying on cache purges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I purge the Cloudflare cache?
&lt;/h3&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Cloudflare lets you purge cached resources from its dashboard, and it also exposes cache-purge APIs.&lt;/p&gt;

&lt;p&gt;When possible, I prefer purging only the specific URL that changed rather than clearing the entire cache.&lt;/p&gt;

&lt;p&gt;But remember: purging Cloudflare removes Cloudflare's cached copy.&lt;/p&gt;

&lt;p&gt;It does not delete a file that a user's browser has already cached according to a long &lt;code&gt;max-age&lt;/code&gt; or &lt;code&gt;immutable&lt;/code&gt; policy.&lt;/p&gt;

&lt;p&gt;Again, when immediate invalidation matters, changing the URL is usually the safest option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Static file caching becomes much simpler once you stop treating every static-looking file the same way.&lt;/p&gt;

&lt;p&gt;Nuxt's hashed build files can be cached for a very long time because every content change creates a new URL.&lt;/p&gt;

&lt;p&gt;Files inside &lt;code&gt;public/&lt;/code&gt; require more care because their URL normally stays the same even if you replace the file.&lt;/p&gt;

&lt;p&gt;Nuxt Image adds another layer because the optimized files requested by the browser live under &lt;code&gt;/_ipx/**&lt;/code&gt;, which needs its own caching policy.&lt;/p&gt;

&lt;p&gt;That last part was what I had missed in my own setup.&lt;/p&gt;

&lt;p&gt;Once I separated those asset types and started thinking about caching based on &lt;strong&gt;whether the URL changes with the content&lt;/strong&gt;, the whole thing became much easier to reason about.&lt;/p&gt;

&lt;p&gt;And Lighthouse stopped complaining for the right reason — not because I blindly made every file immutable, but because the files were actually being cached according to how they behave.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://sadeq.dev/blog/static-file-caching-in-nuxt" rel="noopener noreferrer"&gt;sadeq.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>webperf</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Set Up Rate Limiting in Nuxt</title>
      <dc:creator>Sadeq Sheikhi</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:23:06 +0000</pubDate>
      <link>https://dev.to/sadegh_shaikhi_0549a5c17f/how-to-set-up-rate-limiting-in-nuxt-3lcb</link>
      <guid>https://dev.to/sadegh_shaikhi_0549a5c17f/how-to-set-up-rate-limiting-in-nuxt-3lcb</guid>
      <description>&lt;p&gt;Rate limiting is one of those things that doesn't feel urgent—until someone hammers your login endpoint at 3am and you wake up to a flooded database and a locked-out user base.&lt;/p&gt;

&lt;p&gt;I added this to my Nuxt base layer after realising I'd shipped several projects with zero protection on auth routes. Not great.&lt;/p&gt;

&lt;p&gt;This post walks through the exact setup I now use: Redis-backed, an in-memory fallback when Redis is down, named presets for different sensitivity levels, and a &lt;code&gt;429&lt;/code&gt; page that shows a live countdown instead of just dying on the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure
&lt;/h2&gt;

&lt;p&gt;Three pieces, each with one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;createRateLimiter()&lt;/code&gt; — a factory that builds the limiter, using Redis with an in-memory fallback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;applyRateLimit()&lt;/code&gt; — what you call inside handlers to enforce a limit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server/middleware/rateLimiter.ts&lt;/code&gt; — global middleware so every route gets a baseline for free&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;rate-limiter-flexible ioredis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rate-limiter-flexible&lt;/code&gt; does the heavy lifting: sliding windows, Redis integration, and the insurance fallback pattern we'll use.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The factory
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;server/utils/rateLimiter.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;RateLimiterRedis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RateLimiterMemory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterAbstract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate-limiter-flexible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getRedisClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./redis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="c1"&gt;// Must be unique per limiter, e.g. 'rl:auth'&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="c1"&gt;// Maximum requests within the window&lt;/span&gt;
  &lt;span class="nx"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RateLimitResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="c1"&gt;// Unix timestamp in seconds when the window resets&lt;/span&gt;
  &lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="c1"&gt;// Seconds until retry; 0 if allowed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterAbstract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insurance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimiterMemory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRedisClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;insurance&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimiterRedis&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;storeClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;insuranceLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insurance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Falls back to memory if Redis goes down&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createRateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterAbstract&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLimiter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;RateLimiterAbstract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;limiter&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RateLimitResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLimiter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingPoints&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msBeforeNext&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;thrown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// rate-limiter-flexible throws a RateLimiterRes object,&lt;/span&gt;
      &lt;span class="c1"&gt;// not an Error, when the limit is exceeded.&lt;/span&gt;
      &lt;span class="c1"&gt;//&lt;/span&gt;
      &lt;span class="c1"&gt;// If it throws something else, fail open. A broken limiter&lt;/span&gt;
      &lt;span class="c1"&gt;// should not block every user.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thrown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;msBeforeNext&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[rate-limiter] unexpected error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;thrown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msBeforeNext&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I want to highlight here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy initialization:&lt;/strong&gt; the limiter builds itself on the first request, not at import time. This avoids initialization-order problems in environments where configuration or services may not be ready when modules are first loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail open:&lt;/strong&gt; when Redis throws something unexpected, the request goes through. I would rather have a temporarily unprotected endpoint than have a limiter bug take down the whole application for every user.&lt;/p&gt;

&lt;p&gt;For an especially sensitive system, you may decide to fail closed instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Presets
&lt;/h2&gt;

&lt;p&gt;Not all routes deserve the same treatment. A page view and a password-reset request are very different risks.&lt;/p&gt;

&lt;p&gt;Add named presets at the bottom of the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;NaN&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 60 requests per minute — general API traffic&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiRateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl:api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_API_LIMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_API_WINDOW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 10 requests per 15 minutes — login, register, OTP&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authRateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl:auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_AUTH_LIMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_AUTH_WINDOW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 5 requests per hour — password reset, email verification&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sensitiveRateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;createRateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl:sensitive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_SENSITIVE_LIMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_SENSITIVE_WINDOW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 200 requests per minute — SSR page routes&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageRateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keyPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl:page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_PAGE_LIMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUXT_RATE_LIMITER_PAGE_WINDOW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All limits are overridable through environment variables. You do not need to change the application code to tighten them in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The &lt;code&gt;applyRateLimit()&lt;/code&gt; helper
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;server/utils/applyRateLimit.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;H3Event&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RateLimitResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./rateLimiter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LimiterFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RateLimitResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getClientIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;H3Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;getRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf-connecting-ip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nf"&gt;getRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-real-ip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nf"&gt;getRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setRateLimitHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;H3Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RateLimitResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setResponseHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RateLimit-Limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;setResponseHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RateLimit-Remaining&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;setResponseHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RateLimit-Reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setResponseHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;H3Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LimiterFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;keyFunction&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;H3Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bypassSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NUXT_RATE_LIMITER_BYPASS_SECRET&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suppliedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-rate-limit-bypass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;bypassSecret&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;suppliedSecret&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;bypassSecret&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keyFunction&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;keyFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getClientIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;setRateLimitHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RATE_LIMITED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IP-resolution order matters when your Nuxt application is behind Cloudflare, nginx, or another reverse proxy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;event.node.req.socket.remoteAddress&lt;/code&gt; may contain only your proxy's IP, not the actual client's address. The helper checks common forwarding headers in priority order and falls back to &lt;code&gt;'unknown'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Only trust these headers when requests can reach your application through infrastructure you control. Otherwise, clients may be able to spoof them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;keyFunction&lt;/code&gt; parameter lets you limit requests using something other than an IP address when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Global middleware
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;server/middleware/rateLimiter.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;apiRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/rateLimiter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;applyRateLimit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/applyRateLimit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKIP_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_nuxt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/__nuxt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/_admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/img/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fonts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/js/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/favicon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_ipx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/robots.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sitemap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/og-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;SKIP_PREFIXES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NUXT_RATE_LIMITER_ENABLED&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiRateLimiter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageRateLimiter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every route now gets a baseline limit without touching its individual handler.&lt;/p&gt;

&lt;p&gt;Static assets and Nuxt internals are skipped. Page limiting applies only to &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;HEAD&lt;/code&gt; requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Layering limits on sensitive routes
&lt;/h2&gt;

&lt;p&gt;The global middleware is your floor. For sensitive endpoints, stack a second, tighter limit on top.&lt;/p&gt;

&lt;p&gt;Both limits count down independently, so a request has to pass both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/auth/login.post.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;authRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Continue with authentication...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For password resets and similar endpoints, I key the limiter by email rather than IP.&lt;/p&gt;

&lt;p&gt;An attacker can rotate IP addresses, but the target email remains the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/auth/reset-password.post.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;readBody&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sensitiveRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Continue with the password-reset flow...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also combine the email address and IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getClientIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sensitiveRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct key depends on what you are protecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The &lt;code&gt;429&lt;/code&gt; page
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;app/error.vue&lt;/code&gt;, pull &lt;code&gt;retryAfter&lt;/code&gt; from the error data and show a countdown that reloads the page when it reaches zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;NuxtError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NuxtError&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setInterval&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;onUnmounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"error.statusCode === 429"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Too many requests&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"countdown &amp;gt; 0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Retrying in &lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;countdown&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt; seconds…
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"retry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Retry now
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing else the user needs to do. When the request window resets, the page reloads automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Environment variables
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Set to "false" to disable rate limiting globally
NUXT_RATE_LIMITER_ENABLED=true

# Internal services can send this value through
# the x-rate-limit-bypass header
NUXT_RATE_LIMITER_BYPASS_SECRET=

# Redis configuration
# Leave the host empty to use the in-memory limiter
NUXT_REDIS_HOST=
NUXT_REDIS_PORT=6379
NUXT_REDIS_PASSWORD=

# General API preset
NUXT_RATE_LIMITER_API_LIMIT=60
NUXT_RATE_LIMITER_API_WINDOW=60

# Authentication preset
NUXT_RATE_LIMITER_AUTH_LIMIT=10
NUXT_RATE_LIMITER_AUTH_WINDOW=900

# Sensitive-action preset
NUXT_RATE_LIMITER_SENSITIVE_LIMIT=5
NUXT_RATE_LIMITER_SENSITIVE_WINDOW=3600

# Rendered-page preset
NUXT_RATE_LIMITER_PAGE_LIMIT=200
NUXT_RATE_LIMITER_PAGE_WINDOW=60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/utils/rateLimiter.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Factory and named presets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/utils/applyRateLimit.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-request enforcement, IP extraction, and headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/middleware/rateLimiter.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Global baseline protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app/error.vue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; experience with countdown and automatic reload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The defaults here are conservative. Tune them according to your actual traffic.&lt;/p&gt;

&lt;p&gt;The in-memory fallback means you can ship this before adding Redis and upgrade later without changing the API used throughout your application.&lt;/p&gt;

&lt;p&gt;That is why I keep this implementation in my reusable Nuxt base layer: every new project starts with basic protection already available, instead of waiting until the first abusive request arrives.&lt;/p&gt;




&lt;p&gt;I originally published this tutorial on my personal blog, where I write about Nuxt, TypeScript, infrastructure, and engineering decisions taken from real projects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://sadeq.dev/blog/how-to-set-up-rate-limiting-in-nuxt" rel="noopener noreferrer"&gt;Read the original Nuxt rate-limiting article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
  </channel>
</rss>
