In standalone mode, Next.js prebuilds the ISR cache during the build process. And at runtime, NextServer expects this cache locally on the server. This works effectively when the server is run on a single web server machine, sharing the cache across all requests. In a Lambda environment, the cache needs to be housed centrally in a location accessible by all server Lambda function instances. S3 serves as this central location.
To facilitate this:
- ISR cache files are excluded from the
server-functionbundle and instead are uploaded to the cache bucket. - The default cache handler is replaced with a custom cache handler by configuring the
incrementalCacheHandlerPath(opens in a new tab) field innext.config.js. - The custom cache handler manages the cache files on S3, handling both reading and writing operations.
- Since we're using FIFO queue, if we want to process more than one revalidation at a time, we need to have separate Message Group IDs. We generate a Message Group ID for each revalidation request based on the route path. This ensures that revalidation requests for the same route are processed only once. You can use
MAX_REVALIDATE_CONCURRENCYenvironment variable to control the number of revalidation requests processed at a time. By default, it is set to 10. - The
revalidation-functionpolls the message from the queue and makes aHEADrequest to the route with thex-prerender-revalidateheader. - The
server-functionreceives theHEADrequest and revalidates the cache. - Tags are handled differently in a dynamodb table. We use a separate table to store the tags for each route. The custom cache handler will update the tags in the table when it updates the cache.
Lifetime of an ISR request for a stale page
- Cloudfront receives a request for a page. Let's assume the page is stale in Cloudfront.
- Cloudfront forwards the request to the
server-functionin the background but still returns the cached version. - The
server-functionchecks in the S3 cache. If the page is stale, it sends the stale response back to Cloudfront while sending a message to the revalidation queue to trigger background revalidation. It will also change the cache-control header tos-maxage=2, stale-while-revalidate=2592000 - A new request comes in for the same page after 2 seconds. Cloudfront sends the cached version back to the user and forwards the request to the
server-function. - If the revalidation is done, the
server-functionwill update the cache and send the updated response back to Cloudfront. Subsequent request will then get the updated version. Otherwise, we go back to step 3.
Tags
Tags are stored in a dynamodb table.
There is 3 fields in the table: tag, path, revalidatedAt. The tag field is the partition key and path is the sort key.
We use an index called revalidate with path as a partition key and revalidatedAt as the sort key.
Each tags has several paths, and every subpath is also considered as a tag. For example, if we have a tag tag1 with path /a/b/c, we also have tags /a, /a/layout, /a/page, /a/b, /a/b/layout, /a/b/page, /a/b/c/layout, /a/b/c/page.
When revalidateTag is called, we update the revalidatedAt value for each path and subpath associated with this tag.
When we check if a page is stale, we check the revalidatedAt value for each record and the LastModified of this S3 cache objects . If revalidatedAt is greater than LastModified, we consider the page is stale.
Cost
Be aware that fetch cache is using S3. fetch by default in next is cached, and even for SSR requests, it
will be written to S3. This can lead to a lot of S3 requests and can be expensive. You can disable fetch
cache by setting cache to no-store in the fetch options. Also see this
workaround
get will be called on every request to ISR and SSG that are not cached in Cloudfront, and set will be called on every revalidation.
They can also be called on fetch requests if the cache option is not set to no-store.
There is also some cost associated to deployment since you need to upload the cache to S3 and upload the tags to DynamoDB.
For the examples here, let's assume an app route with a 5 minute revalidation delay in us-east-1. This is assuming you get constant traffic to the route (If you get no traffic, you will only pay for the storage cost).
S3
- Each
getrequest to the cache will result in at least 1GetObject
GetObject cost - 8,640 requests * $0.0004 per 1,000 requests = $0.003456
Total cost - $0.003456 per route per month- Each
setrequest to the cache will result in 1PutObjectin S3
PutObject cost - 8,640 requests * $0.005 per 1,000 requests = $0.0432
Total cost - $0.0432 per route per monthYou can then calculate the cost based on your usage and the S3 pricing (opens in a new tab)
DynamoDB
For the example, let's consider that that same route has 2 tags and 10 paths and subpath for each tag. This is assuming you get constant traffic to the route.
- Each
revalidateTagrequest will result in 1Queryin DynamoDB and aPutItemfor each path associated with the tag, they are grouped in batches of 25 in aBatchWriteItemrequest.
Assuming you do 1 revalidation per 5 minute
Query cost - 8,640 request * $0.25 per 1,000,000 read = $0.00216
BatchWriteItem cost - 86,400 requests * $0.25 per 1,000,000 write = $0.0216
Total cost - $0.04536 per tag revalidation per month- Each
getrequest will result in 1Queryin DynamoDB
Query cost - 8,640 request * $0.25 per 1,000,000 read = $0.00216
Total cost - $0.00216 per route per month- Each
setrequest will result in 1Queryin DynamoDB and aPutItemfor each tag associated with the path that are not present in DynamoDB, they are grouped in batches of 25 in aBatchWriteItemrequest.
Query cost - 8,640 request * $0.25 per 1,000,000 read = $0.00216
Total cost - $0.00216 per route per monthYou can then calculate the cost based on your usage and the DynamoDB pricing (opens in a new tab)