Getting Cloud Tasks Working with Firebase Emulator
Quick plug: I’m building gencmd, a command-line productivity tool that uses Gen AI to supercharge your command-line experience and make you more productive. If you haven’t tried gencmd yet, give it a shot at https://gencmd.com/search to see how it can boost your command-line productivity. Once you are convinced, you can make the experience seamless by running the gencmd cli on the terminal on Windows, Mac, or Linux.
Cloud Tasks, Firebase Integration, and Firebase Emulator
Tech set: I’m using Firebase and Firebase functions in JavaScript.
The Requirement: Once I receive a query from the CLI or the browser, I want to be able to return as quickly to the user as possible and still do logging and aggregate analytics. As it is, Gen AI is quite slow — under a handful of seconds to return a complete response. I don’t want to add to that latency with synchronous, hold-up processing.
The Problem: Once the main user processing is done we should return. Only then should we do any time consuming processing. However, once you return from the Cloud Function in a hook like OnCall, there is no guarantee whether existing promises will run to completion — the entire process could be terminated.
A Possible Solution: We can queue up tasks for processing and pass in the data that we want processed along with the target function that will process it. This works fairly well. You could do Cloud Tasks directly or you can use a simplified integration of Cloud Tasks into Firebase which will take care of some things for you. The documentation for that is here: https://firebase.google.com/docs/functions/task-functions?gen=2nd
But the subsequent Problem: I couldn’t get the Tasks emulator working locally to test it out. I spent more than a day trying to figure out multiple aspects of Cloud Tasks and Firebase integration of Cloud Tasks — I kept seeing multiple variations of this code and it took me a while to realize the difference between these two. Hopefully I have saved you a few hours just on that understanding. Then to get the emulator working for Tasks was even longer. Here I’m just documenting my current working code and the alternatives I tried in case it is useful for somebody else. This github thread gave me some ideas and I’ve added my comments there too: https://github.com/firebase/firebase-tools/issues/4884
After wrestling with different approaches to get Cloud Tasks working in the Firebase emulator for local testing, I found a straightforward solution that works reliably for me. Here’s what worked, followed by approaches that didn’t.
The Working Solution
- Update firebase.json in your project root to enable the Tasks emulator:
...
{
"tasks": {
"port": 9899
}
}
...
2. Create a simple getFunctionUrl implementation:
function getFunctionUrl(name, location = "us-central1") {
if (process.env.FUNCTIONS_EMULATOR === "true") {
return undefined;
}
const projectId = process.env.FIREBASE_CONFIG ?
JSON.parse(process.env.FIREBASE_CONFIG).projectId :
process.env.PROJECT_ID;
return `https://${location}-${projectId}.cloudfunctions.net/${name}`;
}
The code samples you generally see will have a function called getFunctionUrl that devises the endpoint for the task processing. Update your getFunctionUrl as above.
The main thing I noticed is that for Tasks to work in the emulator, you have to return an empty/undefined url.
For the actual firebase server, I tried urls with cloudfunctions.googleapis.com, but those didn’t work. Only the cloudfunctions.net one worked for me.
3. Start/restart the emulator with all required services:
firebase emulators:start --import=./emulator_data/ --export-on-exit=./emulator_data/
The import and export is not necessary. I generally use it as it saves out the data in the firestore emulator.
You should see output confirming the Tasks emulator is running:
i emulators: Starting emulators: auth, functions, firestore, hosting, pubsub, storage, tasks, extensions
...
Cloud Tasks │ 127.0.0.1:9899 │ n/a
At this point, enqueueing tasks worked and Firebase automatically spawned the corresponding function to execute the background processing.
What Didn’t Work
- Google Auth Method: The official documentation suggests using Google Auth to build the URI. While this works in production, it didn’t work in the emulator environment.
- Alternative URLs: Attempts to use cloudfunctions.googleapis.com with both v2beta and v2 API endpoints didn’t work:
// These didn't work
return "https://cloudfunctions.googleapis.com/v2/" +
`projects/${projectId}/locations/${location}/functions/${name}`;
return "https://cloudfunctions.googleapis.com/v2beta/" +
`projects/${projectId}/locations/${location}/functions/${name}`;
Why This Solution Works
The key insight was that the emulator doesn’t need a function URL — returning undefined for the URL when in emulator mode allows the task queue to work properly in the local environment while maintaining production functionality. Is this the recommended method? 🤷♂️/shrug
The simpler approach avoids making external API calls during local development and doesn’t require complex authentication handling, making it more reliable for local testing scenarios.
Trade-offs
While this solution works well for local development, apparently it won’t automatically handle URL changes if Google modifies their function endpoints (as happened in the transition from 1st to 2nd gen functions). (See tzappia’s note here: https://github.com/firebase/firebase-tools/issues/4884#issuecomment-2509011609.) However, for most development scenarios, this trade-off is acceptable given the simplicity of local testing.
Bonus! Code Blueprint for Firebase Enqueue Tasks and Process
I pulled out the relevant skeleton of the code to get Firebase Tasks working, both locally and on the firebase server. Plugin your processing code and you should be able to kickstart your background processing requirement.
const {getFunctions} = require("firebase-admin/functions");
const {onTaskDispatched} = require("firebase-functions/tasks");
const {HttpsError} = require("firebase-functions/https");
const {logger} = require("firebase-functions");
exports.enqueueTask = async function(processTaskFunctionName, data, dispatchDeadlineSeconds = 60) {
const loc = "tasks.enqueueTask()";
if (dispatchDeadlineSeconds < 15) {
throw new HttpsError("invalid-argument", "DispatchDeadlineSeconds for task needs to be a minimum of 15 seconds.");
}
try {
// Get reference to the task queue
const queue = getFunctions().taskQueue(processTaskFunctionName);
// Get the function URL
const targetUri = getFunctionUrl(processTaskFunctionName);
// Enqueue the task
await queue.enqueue(data, {
dispatchDeadlineSeconds: dispatchDeadlineSeconds, // 15 seconds is minimum
uri: targetUri,
});
logger.debug(loc, `Task enqueued successfully to ${processTaskFunctionName}.`);
return Promise.resolve(true);
} catch (error) {
logger.error(loc, `Error enqueueing task to ${processTaskFunctionName}`, error);
return Promise.reject(error);
}
};
function getFunctionUrl(name, location = "us-central1") {
if (process.env.FUNCTIONS_EMULATOR === "true") {
return undefined;
}
const projectId = process.env.FIREBASE_CONFIG ?
JSON.parse(process.env.FIREBASE_CONFIG).projectId :
process.env.PROJECT_ID;
return `https://${location}-${projectId}.cloudfunctions.net/${name}`;
}
exports.postSearchTask = onTaskDispatched({
// Configure retry and rate limiting
retryConfig: {
maxAttempts: 5,
minBackoffSeconds: 10,
},
rateLimits: {
maxConcurrentDispatches: 3,
},
}, async (req) => {
// Get the user data from the task payload
const data = req.data;
// TODO: do the lengthy processing here with the data received
// and log results to a db/pub-sub etc. as there is nothing waiting for this to return
});
At some point in the code, when you’ve done the main processing, you enqueue a task. In my case, just before the gencmd search result is returned to the user, I enqueue the task for doing all the post processing. Note how the name is the name of the function that will perform the processing.
await enqueueTask("postSearchTask", data);
By implementing this solution in gencmd, we’ve been able to handle intensive operations more efficiently, ensuring a smoother experience for our users. If you’d like to see these improvements in action, head over to gencmd and give it a try — your command line productivity will thank you!
Connects: I’m on LinkedIn. If you are interested in Google Cloud and certifications you can also checkout my videos on https://www.youtube.com/AwesomeGCP.