Cloud Functions
A function is a small piece of code that answers on a URL, without you having to think about a server, a container or a Dockerfile. You write a function in JavaScript or Python, publish it as a package, and Cosmos gives it a URL.
Functions cost nothing while nobody calls them: they sleep when idle, wake up on the next request, and spread on more of your nodes when the traffic grows. They are ideal for webhooks, small APIs, form handlers, scheduled jobs and all the glue code that does not deserve a full application.
Before you start
- Functions run on your cluster, so Constellation must be enabled. A single server works.
- The code of a function lives in a Package Registry: create an npm registry for JavaScript functions, or a PyPI registry for Python ones.
- Your nodes need internet access: the dependencies of your functions are downloaded from the public npm or PyPI.
Your first function
Let's make a JavaScript function that says hello. We assume an npm registry answering on npm.mydomain.com, and one of its deploy tokens in your .npmrc (see npm).
1. Write it
A function is a regular npm package. It exports a function that receives the request and the response, in the same style as Express:
index.js
exports.hello = (req, res) => {
const name = req.query.name || "world";
res.json({ message: `Hello ${name}!` });
};
package.json
{
"name": "@mycompany/hello",
"version": "1.0.0",
"main": "index.js",
"dependencies": {}
}
2. Publish it
npm publish --registry https://npm.mydomain.com/
3. Create the function
Go to Constellation > Functions and click Create.
- Function name:
hello - Runtime: Node.js 22
- Registry: your npm registry
- Package:
@mycompany/hello - Handler:
hello, the name of the exported function
Leave the URL section as suggested and click Create.
4. Call it
curl "https://functions.mydomain.com/hello?name=Cosmos"
{"message":"Hello Cosmos!"}
The very first call takes a little longer, while the function installs its dependencies. After that, waking up a sleeping function only takes a moment.
Writing functions
Any HTTP request to the URL of the function calls your handler, whatever the method and the sub-path. The part of the path that identifies the function is removed: a call to /hello/users/12 reaches your code as /users/12.
Cosmos sits in front of your function, so the address of the caller is in the X-Forwarded-For header and the original scheme in X-Forwarded-Proto. Every request also carries an X-Cosmos-Function header with the name of the function.
JavaScript
Available runtimes: Node.js 22 and Node.js 20.
The handler receives (req, res), like an Express route: req.method, req.path, req.query, req.headers, req.body (already parsed for JSON), and res.status(), res.json(), res.send().
exports.contact = async (req, res) => {
if (req.method !== "POST") {
return res.status(405).send("Method not allowed");
}
await saveMessage(req.body.email, req.body.message);
res.json({ ok: true });
};
The handler is looked up in the main file of the package. List what you need in the dependencies of your package.json, it is installed automatically.
Under the hood, functions run on the Functions Framework, the same one Google Cloud and Firebase use. req and res are the real Express objects, so everything Express can do works here too: res.redirect(), res.cookie(), res.set() for headers, piping a stream into res. A function written for Google Cloud or Firebase usually runs on Cosmos as is, once the vendor wrapper is removed.
Checking a webhook signature
Payment providers and git forges sign their webhooks over the exact bytes they sent. Since req.body is already parsed, use req.rawBody for the check:
const stripe = require("stripe")(process.env.STRIPE_KEY);
exports.stripeWebhook = (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Bad signature: ${err.message}`);
}
// ...handle event
res.json({ received: true });
};
Files in your package
Your package is installed like any dependency, and the function does not run from inside its folder: a path such as ./template.html will not be found. Build paths from __dirname instead:
const fs = require("fs");
const path = require("path");
const template = fs.readFileSync(path.join(__dirname, "template.html"), "utf8");
Only the files listed in the files field of your package.json (or everything, when there is no such field) end up in the published package. Keep secrets out of it: a private key belongs in the Environment of the function, not in the tarball.
Native modules
The Node.js runtimes are built on Alpine Linux. Pure JavaScript packages work, and so do native ones that ship a musl build, which most popular ones do. A package that only ships glibc binaries fails to load: check its documentation for Alpine support, or pick an alternative.
Python
Available runtimes: Python 3.12 and Python 3.11.
The handler receives a Flask request, and returns anything Flask accepts: a string, a dictionary (sent as JSON), or a (body, status) pair.
my_function/__init__.py
import os
def hello(request):
name = request.args.get("name", "world")
return {"message": f"Hello {name}!", "greeting": os.environ.get("GREETING", "")}
Package it like any Python project (a pyproject.toml with its dependencies), then build and publish it:
python -m build
twine upload --repository-url https://pypi.mydomain.com/ \
-u __token__ -p "$COSMOS_REGISTRY_TOKEN" dist/*
The handler is looked up in the module named after the package (my-function becomes my_function).
Several functions in one package
A package can export as many handlers as you want. Create one function per handler, all pointing to the same package: each gets its own name, URL and schedule.
Each function is its own container, with its own memory limit and its own copy of the package. Ten tiny functions are ten containers, each sleeping, waking up and installing on its own. When several endpoints belong together, it is often simpler to make them one function and route on the path inside it:
exports.licences = async (req, res) => {
if (req.path === "/create") return createLicence(req, res);
if (req.path === "/renew") return renewLicence(req, res);
res.status(404).send("Not found");
};
With the path /licences, this one function answers on /licences/create and /licences/renew. Keep separate functions for what needs its own limits, its own URL settings or its own schedule.
Running it on your machine
You do not need a cluster to try a function. Cosmos runs functions with the Functions Framework, and the same framework runs on your laptop: it serves one handler of your package on a local port, and sends it every request, whatever the path.
For JavaScript, add it to your dev dependencies, so it stays out of the published package:
npm install --save-dev @google-cloud/functions-framework
npx functions-framework --target=hello --port=8080
curl "http://localhost:8080/?name=Cosmos"
{"message":"Hello Cosmos!"}
For Python, install it in your virtual environment and point it at your module:
pip install functions-framework
functions-framework --target=hello --source=my_function/__init__.py --port=8080
A start script in your package.json makes it a habit:
"scripts": {
"start": "functions-framework --target=hello --port=8080"
}
A few things differ from the real thing. The framework serves one handler at a time, so run it again with another --target to try the next one. There is no Cosmos in front of it: the path arrives untouched (locally you call /users/12, which is what your code sees in production too, since the function prefix is stripped), and headers such as X-Forwarded-For are only there if you send them. Environment variables come from your shell:
GREETING=hi DATABASE_URL=postgres://localhost/app npx functions-framework --target=hello
To reach a managed database or an object storage from your machine, copy the values from its Connection card rather than the ${db.*} template, which only Cosmos expands. Your machine needs to be on the Constellation for them to work.
The create form
- Function name: letters and digits. It identifies the function, and is the default path of its URL.
- Runtime: the language and version.
- Registry, Package, Handler: where the code is, and which exported function to call. The package must be published before you create the function.
- URL: by default all your functions share one Hostname,
functions.followed by your domain, and each has its own Path,/followed by its name. You can change both: give a function its own hostname and empty the path, and it owns the whole hostname. A function can also sit next to an existing website:mydomain.comwith the path/api/contactanswers there, while the site keeps every other path. A hostname of their own is simpler, though: nothing on the site can ever get in the way. Like any other URL, the hostname must point to your server, and Cosmos refuses a hostname and path already taken by another URL. - Environment: variables passed to your code. The template variables of deployments work here, which is the easy way to connect a function to a managed database or an object storage:
| Key | Value |
|---|---|
| DATABASE_URL | ${db.main.app.url} |
| S3_ENDPOINT | ${s3.media.endpoint} |
| S3_ACCESS_KEY | ${s3.media.accessKey} |
| S3_SECRET_KEY | ${s3.media.secretKey} |
Template variables only exist for what Cosmos manages. Anything else, an API key or the connection string of a database hosted elsewhere, goes in as a plain value.
The Advanced section is fine on its defaults:
- Entry: the file (JavaScript) or module (Python) that exports the handler, when it is not the main one. For example
lib/api.jsormy_function.api. - Version: the version of the package to deploy. Empty means the latest published.
- Tags: only the nodes carrying all these tags can run the function. Empty means any node.
- Memory (MB):
256by default. CPUs: no limit by default. - Timeout (seconds):
60by default. Requests running longer are cut. It goes up to 24 hours, for the long jobs run on a schedule. - Idle before sleep:
1hby default. How long a function stays awake without being called (30m,2h...). - Runtime image override: use your own runtime image, for example from a mirror.
Your code can also read COSMOS_FUNCTION (the name of the function), COSMOS_FUNCTION_REV (the release number) and COSMOS_NODE (the node it runs on).
Sleeping, waking up and scaling
One copy of the function is always placed on a node, ready to go, and asleep when idle: it uses no CPU and no memory. The next request wakes it up, and Cosmos holds that request until the function is ready. This is the same mechanism as lazy containers.
Waking up takes a second or two. The very first start of a version is longer, because the package and its dependencies are installed then; Cosmos waits up to three minutes for it. The install is kept, so later wake-ups do not repeat it. If the callers of your function have a short timeout of their own, keep that first start in mind when you deploy.
When the nodes running the function get busy, Cosmos starts copies on more nodes, up to one per eligible node, and spreads the requests between them. When the load drops, the extra copies go away.
The URL of a function answers from any node of your cluster.
Securing the URL
A new function is public, protected by the SmartShield and the bot blocker. The URL tab of the function is the usual Cosmos URL form: from there you can require a login, restrict the function to your Constellation or to a list of IPs, tune the SmartShield, or set CORS.
For a function called by an outside service (a payment provider, a git webhook), keep it public and check a secret in your code. For a function only called by your own applications, restrict it to the Constellation.
Shipping a new version
Publishing a new version of the package does not change what is running: a function always stays on the exact version it was deployed with, until you decide otherwise.
Open the function, and in the Releases card of the Overview tab click Deploy. Choose Latest published version or a specific one. Every copy of the function is replaced with the new version.
The Releases card keeps the history of the deployments, with who did them and when. Roll back next to a release puts that version back in a click.
If your code is in a git repository, CI/CD can publish and deploy your functions on every push.
The Edit tab changes everything else (runtime, package, handler, environment, limits), and the Test invocation card of the Overview tab lets you call the function right from the interface, choosing the method, the path and the body, and shows the answer with its duration.
Running on a schedule
The Triggers tab calls your function on a schedule. Click Add:
- Name: what this schedule is for (
nightly-cleanup). - Schedule: a crontab with seconds first.
0 0 * * * *is every hour,0 0 3 * * *every night at 3. - Method, Path and Body: the request to send. The body is sent as JSON.
The runs and their results appear in the Scheduler page, under Functions. A run is marked as failed when the function answers with an error.
Scheduled calls go through the URL of the function, so the URL must not require a login. To keep a scheduled function private, restrict its URL to the Constellation instead.
A scheduled call is a plain HTTP request from the cluster. Your code can tell it apart from a call made by anyone else with the X-Cosmos-Trigger header, which carries cron: followed by the name of the schedule. This lets one function serve both:
exports.reports = async (req, res) => {
if (req.headers["x-cosmos-trigger"]) {
await sendNightlyReport();
return res.send("done");
}
res.json(await listReports());
};
The call is sent to a node holding a copy of the function. If that node is busy restarting, the next one is tried. Whatever your function answers is kept with the run, so a short summary ("42 emails sent") is worth returning.
Monitoring and events
Each function card shows its status, its version, the nodes holding a copy and its URL. On the function page, the Monitoring tab graphs the number of copies, the invocations (successes, errors, response time, data) and the CPU and memory used. The Events tab keeps the history: created, deployed, updated.
What your code prints (console.log, print) goes to the logs of the container named fn- followed by the function name, visible in the ServApps page of the node running it.
Deleting a function
The Danger zone card of the Overview tab removes the function from every node, along with its URL. The package stays in the registry.
Moving from another platform
Functions written for Google Cloud Functions, Firebase, or as plain Express handlers are the easiest to bring over: remove the vendor wrapper, read your configuration from process.env, fix the file paths as explained above, and publish.
// Before, on Firebase
const functions = require("firebase-functions/v1");
const apiKey = defineString("API_KEY");
exports.hello = functions.https.onRequest((req, res) => { /* ... */ });
// After, on Cosmos
const apiKey = process.env.API_KEY;
exports.hello = (req, res) => { /* ... */ };
Functions are deployments
Behind the scenes, a function is a deployment named fn followed by its name, using the Fill replica mode with the Empty fill mode. You will see it in the Deployments page, with its replicas and its events.
You can also write it yourself: in the deployment form, switch Specification from Compose to Function and describe the package and all its handlers at once. Each handler becomes a function of the Functions page, and they are all deployed and rolled back together:
{
"runtime": "node22",
"source": { "registry": "npm", "package": "@mycompany/api" },
"env": { "DATABASE_URL": "${db.main.app.url}" },
"limits": { "memoryMB": 256, "timeoutSec": 60, "idleTTL": "1h" },
"handlers": [
{
"name": "hello",
"handler": "hello",
"route": {
"Host": "functions.mydomain.com",
"PathPrefix": "/hello",
"StripPathPrefix": true,
"BlockCommonBots": true,
"SmartShield": { "Enabled": true }
}
},
{
"name": "cleanup",
"handler": "cleanup",
"entry": "lib/jobs.js",
"env": { "DRY_RUN": "0" },
"route": {
"Host": "functions.mydomain.com",
"PathPrefix": "/cleanup",
"StripPathPrefix": true,
"RestrictToConstellation": true
},
"triggers": {
"cron": [
{ "name": "nightly", "enabled": true, "crontab": "0 0 3 * * *", "method": "POST", "path": "/", "body": "{}" }
]
}
}
]
}
The env of the deployment is shared by every handler; a handler can add its own, which wins when the same key is in both.
Choose the Fill replica mode with the Empty fill mode to get the same sleep and scale behaviour as the functions created from the Functions page.