How to reduce fast origin transfer

Fast origin transfer is the highest cost (by far) on our NextJS app at about 1GB per day. We’re growing, and want to prevent this from causing a cost spike in the near future.

In our app, some pages fetch data directly from the server, while some others use API routes. The vast majority of the data fetch happens on the server. These are the expensive operations in terms of data transfer. We have inspected all of the queries and reduced the data transfer to a minimum. Most of this data can be cached for long periods of time (1 day or more), so the expensive computation and transfer from origin shouldn’t be necessary in theory.

In an attempt to reduce fast origin transfer, we’ve tried a couple of things. Firstly, using ISR with dynamic params (since there’s 25k+ possible routes and we can’t precompute every single one at build time). However, this has 2 big problems: it is way more expensive than fast data transfer and the UX is pretty awful (when loading a route with params that haven’t been loaded before, the loading state does not show up - feels stuck - there’s open issues for this already on GH). Secondly, we wrapped all of the expensive operations with unstable_cache. The cache definitely works but fast origin transfer is not reduced by a single byte. I would expect the cache to not have to refetch from origin.

The biggest problem, however, is that we can see fast origin transfer in the Usage tab of our project, but we can’t see anything specific to fast origin transfer in Observability. It’s currently impossible to know what paths are the major culprits without extensive debugging and trial and error. If we were certain a specific path is the culprit, we can change data loading to happen via API and cache response headers as suggested in the docs. In any case, feels like we have to work around Vercel + NextJS to avoid huge costs.

Fast Origin Transfer is the data used by

  • Vercel Functions
  • Middleware
  • Runtime Cache

You didn’t mention which project this is, but if I’ve found the correct one (with near 1GB of Fast origin transfer each day) then it’s not using Vercel Functions or Runtime Cache (usually ISR), leaving only Middleware

You can check the middleware paths in observability /observability/middleware?sortBy=requests&sortDesc=1&tab=requestPath&period=30d if your team has access to it

We unfortunately don’t have any way to sort by bandwidth or duration (I will bug internally about improving this) but you can sort by number of invocations, and the /api/search/search_with_id route is the most invoked at 3x the requests to your root route /, so I would start there with optimizing

  • Put guards at the top (auth redirects) to do as little unnecessary work as possible
  • Trim your responses to only return the minimum necessary data. If this is for a search result page, only include fields that appear on the UI

Our docs for optimizing fast origin transfer are here

2 Likes

Thanks @jacobparis !

Part of the fast origin transfer is due to apis, but I think the vast majority comes from SSR pages. These pages fetch data on the server (calling db query functions directly) and pass that down to the client to render.

I tried adding unstable_cache on top of all of these queries but it seems to have no impact. If I understand correctly, the only way to leverage data cache and prevent fast origin transfer in this case would be to call the API route with the full path and return cache headers?

Unfortunately, ISR is a no go. Tried it briefly to render individual listings but it’s just too expensive.

Is there no other way to prevent fast origin transfer on these repeated queries?

Thanks!

As a further update of my post above, I’ve reduced the data sent from my most frequently called functions by at least 30% but am seeing absolutely no change in fast origin transfer usage. Right now, it’s impossible to understand fast origin transfer from the Vercel usage or observability dashboards.

Via Vercel CDN overview:

The Fast Origin Transfer costs you are incurring are because the responses to your Edge Requests are not being served from the Vercel CDN. Each of your requests is starting on the left side of the diagram and making it all the way to the right side of the diagram, which is the most expensive type of request. You can make your Vercel application cheaper to run by keeping requests as far left as possible (i.e: have the response served by the Vercel CDN) and keep the amount of data included in responses to a minimum.

(The unstable_cache method (soon to be use cache) is used within your functions, it does not have an influence on Fast Origin Transfer or Fast Data Transfer. You can use unstable_cache to reduce Function Duration (because less work is being done) but not Fast Origin Transfer (it doesn’t change how much data your users receive).)

A static pre-rendered frontend[1] and API endpoints that return only essential data with response caching should get you to where you want to be. If you look at Route Handlers and Middleware you’ll see that there is no caching by default for API routes, you need to either implement it yourself (by returning the appropriate response headers to instruct Vercel CDN to cache the response) or you can use the NextJS ISR features (with things like revalidatePath to clear the cache when the underlying data changes).

[1] The in development partial pre-rendering feature is an interesting way to achieve while keeping the benefits of server-side rendering. The most traditional version thought is an SPA which you can easily achieve with NextJS today.

edit: I did a little sleuthing and I think I found your website. If I’m looking at the right site, the issue is that you aren’t using any response caching, i.e: everything is bypassing Vercel’s CDN which means you have the most expensive setup. Something as simple as putting export const revalidate = 86400 at the top of /buscar/[searchId]/[propertyId]/page.tsx with generateStaticParams returning an empty array should reduce your Fast Origin Transfer substantially. Basically, you just need generateStaticParams and revalidatePath – unless you want to switch to an SPA which would have an even greater impact :slight_smile:

2 Likes

Hey @shrink, thanks for the detailed reply :slight_smile:

That makes things a lot clearer, although I still have some questions / doubts.

The project you mention is the correct one, and that particular route /buscar/[searchId]/[propertyId]/page.tsx is likely the culprit of the fast origin transfer, since it’s data heavy and is the most loaded route in my app.

It is an SSG page where content rendered varies according to a session cookie. There are 2 variants of each page. On the server, the page calls 2 functions to fetch data: getProperty and getOwners. These are regular tsx functions running on the server, with no additional wrapper.

One of the first things I did to try and reduce fast origin transfer was to cut down the amount of data being returned by these, since we were returning unnecessary data. The expensive function in terms of data usage is getProperty, the other one is a lot lighter. We reduced the amount of data, on average, by at least 30% but are seeing no impact at all in fast origin transfer usage. Currently from the dashboards, it’s impossible to know whether the change helped, and if it didn’t, why not. All I can see is that fast origin transfer (on usage dash) remains constant days after.

I’ve tried ISR with the following setup before, for different routes.

export const revalidate = 86400

export async function generateStaticParams() {
    return []
}

The problem we ran into was a noticeable lag each time a new set of params is being rendered (I assume because of lack of PPR in stable), and ISR costs skyrocket (a lot higher than fast origin transfer).

In our case, we have multiple functions where it would make sense to cache the result at CDN level. If I understand correctly, the only way to do this is by exposing the function logic in an API route and adding cache headers. There’s no way to cache the result if I’m simply calling a regular function server to server. Is that so?

Thank you!

Your code’s functions (e.g: getProperty) are executed on the server, you do not pay for the data returned from functions within the execution of a Function. Fast Origin Transfer is the data you return in response to a request. I think some confusion might be coming from the word “function”. Vercel’s use of the word “Function” refers to the Vercel serverless product, you could replace the word “Function” with “execution” or “response” or “origin” or “server” to get a more unambiguous reading, e.g: “Fast Origin Transfer is the data transferred between Vercel’s CDN and your [Function | execution | response | origin | server]”.

For example, imagine we have a NextJS page that accepts a length parameter:

function generateStringOfLength(length: number = 128) {
  return "x".repeat(length);
}

export default async function Page(props: PageProps<'/example/[length]'>) {
  const { length } = await props.params

  const generatedString = generateStringOfLength(length);
  
  return <div>You have requested a string.</div>
}

You will use exactly the same amount of Fast Origin Transfer for any request to this page because every request returns a response with the same amount of data. The value of generatedString could be billions of bytes but it is not included in the response so it doesn’t influence Fast Origin Transfer. Each of the following requests would incur exactly the same fast Origin Transfer:

/example/1
/example/128
/example/1024
/example/1000000000

If we now modify the Page to return the string generated, then the Fast Origin Transfer will change:

function generateStringOfLength(length: number = 128) {
  return "x".repeat(length);
}

export default async function Page(props: PageProps<'/example/[length]'>) {
  const { length } = await props.params

  const generatedString = generateStringOfLength(length);
  
  return generatedString;
}

The Fast Origin Transfer for this will now scale according to the length of the generated string, because the longer the string, the more data transferred to the user:

/example/1 -> 1 byte of Fast Origin Transfer
/example/128 -> 128 bytes of Fast Origin Transfer
/example/1024 -> 1kb of Fast Origin Transfer
/example/1000000000 -> 1 gb of Fast Origin Transfer

If we go to one of your website pages (e.g: /buscar/montevideo/6710528) and look in our network tab, we can see that the response size is 22.4kb so each visit to /buscar/montevideo/6710528 is incurring 22.4kb of Fast Origin Transfer because that is the data being returned from your Function.

Conceptually, you reduce costs (and most benefit from Vercel) by moving as much of the stable / static content (where “content” means “things returned in response to a request” like HTML) to the Vercel CDN and have your Functions do only the minimum required to serve unique content per-request. So for your application, you would have a static layout made up of the 22.4kb of HTML you’re currently returning on each request and then when a user visits the page you would make an API request from the browser to get the data which you then hydrate the HTML with.

edit: If you add the export const revalidate = 86400 configuration then Vercel will store the Function’s response in the CDN’s cache and any subsequent request will be served from Vercel’s CDN, incurring only Fast Data Transfer, instead of both Fast Origin Transfer and Fast Data Transfer. After the revalidation period is reached (24 hours) then Vercel will execute the Function again. So setting up caching + revalidation for /buscar/[searchId]/[propertyId]/page.tsx in your current system will reduce how often your Function is executed by bypassing it on the second (and third and fourth etc.) requests within the revalidation period. That said, if the pages on your website typically only receive 1 visit within the revalidation period, then adding caching would not have a noticeable impact on Fast Origin Transfer because the pages are expiring and the Function is being executed every time.

edit edit: also I don’t work for Vercel, just in case it is unclear because I mentioned your website’s file path which might suggest I can see your codebase. I can see your file path because Vercel returns it in your request headers. I’m just another customer!

Thanks for the extra context @shrink, super clear!

The page in question is an SSG page that calls db twice and uses cookies to decide which version of the page to render. We fetch the data on the server and then render a client side component for the full listing, using that data.

Because of the use of cookies, and because ISR is too expensive on Vercel, I don’t think it’s a viable solution for us at the moment. Vercel seems to invalidate the ISR cache on every deploy, and pre rendering 25k+ pages at build time is just not possible. The data needs to be revalidated every 1-2 hours max, so that would also play against ISR. It’s likely we wouldn’t see a huge benefit from it.

Right now, the only solution that seems viable would be to rearchitect the app to not use SSG at all. However, I don’t want to go through that before actually being able to understand what’s causing the high fast origin transfer in the first place. Unfortunately, the Vercel dashboards don’t show any useful insights.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.