Overcoming Next.js Vercel Timeout With Firebase Functions

Ali BaderEddin
Better Programming
Published in
14 min readMay 2, 2023

--

In my current Next.js web app, I have two API endpoints:

  1. /api/generateExerciseData, which calls OpenAI API with gpt-3.5-turbo to generate exercise descriptions, categories, subcategories, intensities, and tips.
  2. /api/generateWorkout, which calls OpenAI API with gpt-4 to generate workouts based on user prompts.

While these endpoints work seamlessly in my local environment, they encounter issues in the Vercel production environment due to the ten-second Serverless Function Execution Timeout.

I could upgrade from the Hobby plan to the Pro plan for $20/month to increase the timeout to 60 seconds, but the GPT-4 endpoint may take up to two minutes to run. Additionally, I’m reluctant to spend $240/year just for this single feature.

Furthermore, I have an existing mobile app that I would like to integrate with those capabilities at some point in the future.

Considering the requirements above, I have decided to migrate my API endpoints from Vercel Serverless Functions to Firebase Cloud Functions.

In this article, I will guide you through overcoming Vercel’s Serverless Function Execution Timeout limitation by replacing them with cost-effective Firebase Cloud Functions.

Creating Firebase Cloud Functions

Initialization

Install Firebase CLI and initialize Cloud Functions in your project:

npm install -g firebase-tools
firebase login
firebase init functions

The firebase init functions command initializes Firebase Cloud Functions in your project. This command configures and files to create and deploy serverless functions using Firebase.

Note: I ran it on my existing Next.js code repository, but I recommend starting a new repository for the Firebase functions.

Edit: I did end up deciding to move the functions to their own repostiroy to fully separate Vercel from Firebase, especially deployments and configs.

When you run firebase init functions, the Firebase CLI guides you through an interactive setup process:

  1. It asks you to select the Firebase project you want to associate with your functions.
  2. It prompts you to choose your preferred language for writing functions (JavaScript or TypeScript). I chose TypeScript.
  3. It creates a functions folder in your project’s root directory, which will contain all the necessary files and configurations for your Cloud Functions.
  4. It generates a package.json file and a node_modules folder inside the functions directory, including the required dependencies for your functions.
  5. If you choose TypeScript, it also sets up a tsconfig.json file to configure TypeScript compilation.

After running firebase init functions, you can start developing your Firebase Cloud Functions by adding your code to the index.js (or index.ts for TypeScript) file inside the functions folder. Once you have written your functions, you can deploy them using the firebase deploy — only functions command.

Creating the Functions

Next, create the new functions inside the functions directory (created during initialization). For example, in functions/index.ts:

import * as functions from "firebase-functions";


exports.generateExerciseData = functions.https.onRequest(async (req, res) => {
res.send("Generates exercise data by calling OpenAI with gpt-3.5-turbo");
});


exports.generateWorkout = functions.https.onRequest(async (req, res) => {
res.send("Generates workouts by calling OpenAI with gpt-4");
});

For my own functions, I copied and pasted the code and the imports from my existing Next.js APIs. I got several errors from Firebase ESLint rules, and fixing them got so annoying and counterproductive, so I just ended up disabling the rules by setting strict to false in the tsconfig.json file in the functions folder.

Testing the Functions Locally

We can use the Firebase Emulator Suite to test the Firebase Cloud Functions locally. Here’s how to set it up and run your functions locally:

Navigate to your project’s root directory and set up the emulators:

firebase init emulators

When prompted, select “Functions” and follow the prompts to set up the Functions Emulator. Start the Firebase Emulator Suite:

firebase emulators:start

This will start the emulators, including the Functions Emulator.

Call your function locally using a tool like curl or Postman. Use the URL provided in the emulator output (e.g., http://localhost:5001/<your-project-id>/us-central1/<function-name>).

Functions Emulator Troubleshooting

On running the firebase emulators:start command, I got an error:

functions: Failed to load function definition from source: FirebaseError: There was an error reading functions/package.json:

functions/lib/index.js does not exist, can’t deploy Cloud Functions

The Firebase Functions emulator cannot find the required index.js file in the functions/lib directory. I missed building the TypeScript functions before starting the emulator. You can run the build command in the functions directory with this code:

cd functions
npm run build

This will transpile the TypeScript code to JavaScript and output it in the functions/lib directory. Once the build process is complete, start the Firebase emulators again:

firebase emulators:start

I saw this warning, so I updated the package.json to point to Node 18 rather than 16.

functions: Your requested “node” version “16” doesn’t match your global version “18”. Using node@18 from host.

Now, the Functions emulator started without issues.

I ran the following curl command to test one of the functions:

curl -X POST -H "Content-Type: application/json" -d '{"userPrompt": "I want something basic that can help me get comfortable upside down with some wall assistance.", "exerciseList": "do your magic"}' http://127.0.0.1:5001/handstand-quest/us-central1/generateWorkout

I got this response.

{"error":"Request failed with status code 401"}

The error “Request failed with status code 401” indicates the request is unauthorized.

Firebase vs Next.js Environment Variables

When I copied the code earlier, this is how I pulled the API key from OpenAI as an environment variable.

const configuration = new Configuration({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
});

Firebase functions have a different way of handling environment variables than Next.js. I had to use Firebase’s environment configuration to set environment variables for the Firebase functions.

To set the environment variable for the Firebase project, I ran the following command in the terminal:

firebase functions:config:set openai.api_key="your_openai_api_key"

I got this output, but I did not deploy it yet since I am still testing locally.

✔ Functions config updated.

Please deploy your functions for the change to take effect by running firebase deploy — only functions

In the Firebase function, I accessed this environment variable using functions.config() like this:

const apiKey = functions.config().openai.api_key;

I rebuilt the Firebase functions code and started the emulator again, and now I saw this error:

Failed to load function definition from source: FirebaseError: Failed to load function definition from source: Failed to generate manifest from function source: TypeError: Cannot read properties of undefined (reading ‘api_key’)

Deploying the Functions

I thought maybe I should deploy and check if Firebase “local” emulator would now pick up the environment variable.

firebase deploy - only functions

On deployment, I received the following errors.

/Users/alib/Code/GitHub/hq-exercise-catalog/functions/lib/index.js

4:1 error Parsing error: The keyword ‘const’ is reserved

/Users/alib/Code/GitHub/hq-exercise-catalog/functions/src/index.ts

1:1 error Parsing error: The keyword ‘import’ is reserved

✖ 2 problems (2 errors, 0 warnings)

The ESLint configuration was not recognizing the ES6 syntax. I updated the ESLint configuration to support ES6 syntax by following these steps to fix this.

First, ensure you have the following packages installed:

npm install - save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Create or update the .eslintrc.js file in the root of your project with the following content:

module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
// Add any custom rules here
},
};

This configuration sets up ESLint to use the TypeScript parser and apply the recommended rules for TypeScript. After making these changes, the ESLint configuration should recognize the ES6 syntax and not throw errors related to the const and import keywords.

However, I was still getting the following error on the .eslintrc.js file:

‘module’ is not defined.

It appears the module keyword is causing an issue with the ESLint configuration. To fix this, I replaced the .js file with a .eslintrc.json file with the following content:

{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
// Add any custom rules here
}
}

On deployment, I got an even larger list of errors (summarized below):

/Users/alib/Code/GitHub/hq-exercise-catalog/functions/lib/index.js

2:23 error ‘exports’ is not defined no-undef

5:18 error Require statement not part of import statement @typescript-eslint/no-var-requires

5:18 error ‘require’ is not defined no-undef

29:44 error ‘setTimeout’ is not defined no-undef

131:5 error ‘console’ is not defined no-undef

✖ 12 problems (12 errors, 0 warnings)

Can you imagine how much time was wasted because of ESLint? Anyways, I updated the config once more:

{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
"node": true
},
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-console": "off"
}
}

OK, it seems happy with the deployment now. Well, at least the ESLinting part. To sum up the deployment logs, there was this lingering issue in it:

Could not load the function, shutting down.

Please visit https://cloud.google.com/functions/docs/troubleshooting for in-depth troubleshooting documentation.

Function failed on loading user code. This is likely due to a bug in the user code. Error message: Provided module can’t be loaded.

Did you list all required modules in the package.json dependencies?

Detailed stack trace: Error: Cannot find module ‘openai’

Oops, I forgot to update the packages.json file. I am wondering how the npm run build passed! Anyways, I added the dependency, did npm install and npm run build, then deployed again. Finally, it succeeded.

Function URL (generateExerciseData(us-central1)): https://us-central1-handstand-quest.cloudfunctions.net/generateExerciseData

Function URL (generateWorkout(us-central1)): https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout

✔ Deploy complete!

Project console: https://console.firebase.google.com/project/handstand-quest/overview

Now, I want to get back to the local environment testing to ensure things work as expected. I started the emulator and then re-ran the curl command.

I still got the same error in the emulator.

Failed to load function definition from source: FirebaseError: Failed to load function definition from source: Failed to generate manifest from function source: TypeError: Cannot read properties of undefined (reading ‘api_key’)

This is when I realized that “deployment” is not related to the local environment. Common sense, but sometimes, I don’t have that.

Local Firebase Env Variables

It turns out the environment variables need to be fed to the emulator via a config file.

Fetch the current environment configuration and save it as a JSON file:

firebase functions:config:get > .runtimeconfig.json

This command will create a .runtimeconfig.json file in your functions directory. Add this file to your .gitignore to prevent sensitive data from being committed to your repository.

Finally, it’s all working when the emulator starts. I reran my curl command from the terminal:

curl -X POST -H "Content-Type: application/json" -d '{"userPrompt": "I want something basic that can help me get comfortable upside down with some wall assistance.", "exerciseList": "do your magic"}' http://127.0.0.1:5001/handstand-quest/us-central1/generateWorkout

It’s working the way I expect!

Replacing Next.js APIs with Firebase Functions

Now that we’ve set up the Firebase functions and tested them locally for functionality, it’s time to update the Next.js application to call the Firebase Cloud Function instead of the Vercel API endpoint.

The Firebase Cloud Function URL will have the following format:

https://<region>-<project-id>.cloudfunctions.net/<function-name>

In my case, for one of my functions, this looks like this:

https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout

Now, in my Next.js code, I’ll simply replace the relative path of /api/generateWorkout with the full path https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout. After that, I’ll do the same for the other function — a very, very simple change.

const fetchGpt4Data = async (userPrompt, exerciseList) => {
const API_URL = "/api/generateWorkout";


const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userPrompt,
exerciseList,
}),
};

Firebase Functions in Production

Before testing the behavior of the functions within the Next.js web app, it’s probably wise to test this endpoint directly from the terminal first.

curl -X POST -H "Content-Type: application/json" -d '{"userPrompt": "I want something basic that can help me get comfortable upside down with some wall assistance.", "exerciseList": "do your magic"}' https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout

Unfortunately, I received this error:

B-Pro:~ alib$ curl -X POST -H “Content-Type: application/json” -d ‘{“userPrompt”: “I want something basic that can help me get comfortable upside down with some wall assistance.”, “exerciseList”: “do your magic”}’ https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout

<title>403 Forbidden</title>

<h2>Your client does not have permission to get URL <code>/generateWorkout</code> from this server.</h2>

I noticed (with help from Stack Overflow) a “private” setting in the package.json in the functions folder, so I set it to false and redeployed. Didn’t work!

When I tested the function in the Firebase Cloud Function console, it worked as expected.

https://console.cloud.google.com/functions/details/us-central1/generateWorkout?env=gen1&tab=testing&authuser=0&project=handstand-quest

I found steps from the same Stack Overflow thread that worked!

  • Go to the Google Cloud Console (Not Firebase Console) -> Search For Cloud Functions to see the list of functions
  • Click the checkbox next to the function you want to grant access to.
Select your Firebase functions from the Google Cloud Console
  • Click Permissions at the top of the screen. The Permissions panel opens.
Manage permissions of Firebase functions
  • Click “Add principal”
  • In the “New principals” field, type “allUsers”
  • Select the role “Cloud Functions” > “Cloud Functions Invoker”
  • Select a role dropdown menu
Cloud Functions Invoker to allUsers
  • Click Save.

Wisely, Google shows a message to prevent you from making this decision to make your cloud functions insecure.

Functions are now publicly accessible to everyone

Although insecure, I proceed with this option until I guard my web app with login/signup capabilities.

With this change, the curl request to the public endpoint works as expected. Now that I have the endpoint running, I will test call it from the app.

Vercel Deployment

Let’s test the change in the staging environment.

Since I initialized the Firebase functions folder in the Next.js project that gets deployed to Vercel; I need to tell Vercel to ignore this new folder. This is the error I currently get on deployment to Vercel.

Type error: Cannot find module firebase-functions or its corresponding type declarations.

To tell Vercel to ignore a folder, we can add a .vercelignore file to the root of our project and add the folder or files we want to ignore.

I added the following line to the .vercelignore file and pushed the change to the git branch, so Vercel could automatically handle the deployment to the staging environment.

functions

Side note: If I deployed the Firebase functions to their own codebase, I wouldn’t have had to deal with this. Luckily, it’s a simple change to ignore the functions folder.

When testing in the staging environment, I got this error:

Access to fetch at ‘https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout' from origin ‘https://hq-exercise-catalog-bl7zti3ol-alibad.vercel.app' has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

Fixing CORS Issues

I ran the Next.js local environment and went ahead and did some testing. I got the standard CORS error from localhost. It made me appreciate what Next.js/Vercel is doing behind the scenes.

Access to fetch at ‘https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout' from origin ‘http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

Side note: This reminded me… It’s really expensive to build secure software, and by design, it slows companies down and has a major impact on reducing the innovation rate. Still, it’s necessary and the responsible thing to do. Building a side project, I don’t need to be that responsible!

The error message is a CORS (Cross-Origin Resource Sharing) issue. This issue happens when you try to make a request from a different origin (in this case http://localhost:3000) to a server (https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout) that doesn’t have the necessary headers to allow requests from different origins.

I added the CORS headers to the Cloud Functions to resolve this issue.

First, install the CORS npm package:

npm install cors

Wrap the login in each function with this:

corsHandler(req, res, async () => {
// current function code
}

This will deploy the Cloud Functions with the updated CORS headers. With the above steps, the Cloud Functions should now allow requests from different origins.

Unfortunately, when I tried to call the Firebase Cloud Function from my Web App, I still got an error saying the CORS policy blocked that access. So, where is the CORS policy defined?

Let’s look at the requests and responses in the Network tab in the Google Chrome Developer Tools.

There is an initial preflight request, as shown below:

OPTIONS https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout
OPTIONS 204

The OPTIONS request has an HTTP 204 from the server, with the following properties in the response header.

access-control-allow-headers: content-type
access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE
access-control-allow-origin: https://hq-exercise-catalog-bl7zti3ol-alibad.vercel.app

The OPTIONS request is then followed by the POST Request.

POST https://us-central1-handstand-quest.cloudfunctions.net/generateWorkout
POST 408

The response for the POST request is an HTTP 408.

HTTP 408 status code, also known as “Request Timeout,” indicates the server did not receive a complete request from the client within the server’s allotted time limit.

This means Firebase is timing out too, and the resulting browser error is misleading!

The response also includes a Referrer Policy set to strict-origin-when-cross-origin.

strict-origin-when-cross-origin

Regarding the Referrer Policy set to strict-origin-when-cross-origin, this security feature controls how much referrer information should be included when a user navigates from one origin to another. It’s not directly related to the timeout issue or the CORS configuration, so I will ignore it.

Firebase functions timeout limit

So Firebase also happens to have that timeout. However, the difference is that Firebase lets you configure it without subscribing to a $20-per-month plan. By default, Firebase Cloud Functions have a timeout limit of 60 seconds. You can increase this limit to 540 seconds (nine minutes).

For the generateExerciseData function, the 60-second timeout is good enough since gpt-3.5-turbo is much faster than gpt-4, which is used in the generateWorkout function.

Increasing the timeout limit may result in higher resource usage and, thus, costs. However, I’m willing to try and see how that plays out. I highly doubt it’ll hit $20 every month. We’ll see how that goes with time when traffic patterns change. For now, I’m expecting it to cost less than $1 a month.

To change the amount of timeout in the generateWorkout Firebase function, let’s update the function definition in the index.ts file. I think I want it to wait up to three minutes.

exports.generateWorkout = functions
.runWith({ timeoutSeconds: 180 })
.https.onRequest((req, res) => {
// Your existing generateWorkout function code
});

I deployed the change to Firebase and finally got unblocked!

Streaming (Timing in)

To speed up the whole experience and make it easier to enjoy, streaming the GPT data and gradually reflecting that in the UI would be a good idea. This will reduce the time it takes to start seeing data, and it will avoid timeouts in the first place.

Upon reflection, it became clear that the current design approach was not optimal, which led to the need for a workaround to address the timeout issue. A more efficient approach would have been to deliver the data in smaller, more manageable chunks from the outset.

Time is a critical factor, and while transitioning from Next.js APIs to Firebase Functions, authentication and CORS issues caused a delay.

In my upcoming article, I will explore whether streaming is a more effective design strategy. It’ll all depend on how long it will take and whether unreasonable blockers come up.

It will be tricky, though, since my internal implementation gets GPT to return the exact JSON format, which would be hard to stream.

Conclusion

I hope this article gave you the idea and the details on how to update your Next.js application to use Firebase Cloud Functions instead of Vercel API endpoints.

By making this simple switch, you can save a significant amount in yearly costs without sacrificing the functionality of your application.

Although Vercel is an excellent platform, the ten-second timeout can be a hindrance. Nonetheless, I remain a big fan of Vercel and appreciate its numerous benefits.

Vercel Love generated by Adobe Firefly

--

--