Optimizing Networking

The simplicity of Cloud Functions lets you quickly develop code and run it in a serverless environment. At moderate scale, the cost of running functions is low, and optimizing your code might not seem like a high priority. As your deployment scales up, however, optimizing your code becomes increasingly important.

This document describes how to optimize your functions' networking. Some of the benefits of optimizing networking are as follows:

  • Reduce CPU time spent in establishing new connections at each function call.
  • Reduce the likelihood of running out of connection or DNS quotas.

Maintaining Persistent Connections

This section gives before-and-after examples of how to maintain persistent connections in a function. Failure to do so can cause you to quickly exhaust your connection quota.

The scenarios covered in this section are HTTP/HTTPS, Axios and Superagent packages, and accessing Google APIs.

HTTP Requests with HTTP/HTTPs Packages

This section shows before-and-after examples of how to optimize a function that makes HTTP requests with HTTP/HTTPs packages.

Before Optimization

The following function performs a new connection on every function invocation:

var http = require('http');

exports.function = function (request, response) {
    req = http.request({
        host: '<HOST>',
        port: 80,
        path: '<PATH>',
        method: 'GET',
    }, (res) => {
        let rawData = '';
        res.setEncoding('utf8');
        res.on('data', (chunk) => { rawData += chunk; });
        res.on('end', () => {
            response.status(200).send('Data: ' + rawData);
        });
    });
    req.on('error', (e) => {
        response.status(500).send('Error: ' + e.message);
    });
    req.end();
};

After Optimization

This optimized version of the above code snippet maintains persistent connections by using a custom HTTP agent with the keep-alive option:

var http = require('http');
var agent = new http.Agent({keepAlive: true});

exports.function = function (request, response) {
    req = http.request({
        host: '<HOST>',
        port: 80,
        path: '<PATH>',
        method: 'GET',
        agent: agent,
    }, (res) => {
        let rawData = '';
        res.setEncoding('utf8');
        res.on('data', (chunk) => { rawData += chunk; });
        res.on('end', () => {
            response.status(200).send('Data: ' + rawData);
        });
    });
    req.on('error', (e) => {
        response.status(500).send('Error: ' + e.message);
    });
    req.end();
};

The same approach works for HTTPS—just use https.Agent instead of http.Agent.

HTTP Requests with an Axios Package

This section shows before-and-after examples of how to optimize a function that makes HTTP requests with an Axios package.

Before Optimization

The following function requires one connection and two DNS resolutions for every invocation:

var axios = require('axios');

exports.function = function (req, res) {
    axios({
        method: 'get',
        url: '<URL>',
        responseType: 'text'
    }).then(function(response) {
        res.status(200).send('Data: ' + response.data);
    });
};

Package.json

{
    "dependencies": {
        "axios": "^0.15.3"
    }
}

After Optimization

The number of connections are reduced if you create a global Axios instance and a custom HTTP agent that maintains persistent connections. Now, in steady state, there are no connections or DNS queries per function invocation:

var axios = require('axios');
var https = require('https');

const instance = axios.create({
    baseURL: '<URL>',
    timeout: 10000,
});

const requestConfig = {
  method: 'get',
  url: '',
  httpsAgent: new https.Agent({ keepAlive: true }),
};

exports.function = function (req, res) {
    return instance.request(requestConfig).then(function(response) {
        console.log('Data: ' + response.data);
        res.status(200).send('Data: ' + response.data);
    });
};

A similar approach also works for HTTP.

HTTP Requests with a Superagent Package

This section shows before-and-after examples of how to optimize a function that makes HTTP requests with a Superagent package.

Before Optimization

The following function uses one connection and two DNS resolutions for every invocation:

var request = require('superagent');

exports.function = function (req, res) {
    request
        .get('<URL>')
        .end(function(err, response) {
            res.status(200).send('Data: ' + response.text);
    });
};

After Optimization

To keep persistent connections, it is enough to provide a custom HTTP agent for each request:

var request = require('superagent');
var https = require('https');
const keepAliveAgent = new https.Agent({ keepAlive: true });

exports.function = function (req, res) {
    request
        .get('<URL>')
        .agent(keepAliveAgent)
        .end(function(err, response) {
            res.status(200).send('Data: ' + response.text);
    });
};

Accessing Google APIs

This section shows before-and-after examples of how to optimize a function that accesses Google APIs.

Before Optimization

This example uses Cloud Pub/Sub, but this approach works also for other client libraries—for example, language API client or Cloud Spanner. However, note that the savings may depend on the current implementation of a particular client library.

This code performs one connection and two DNS queries per invocation:

const PubSub = require('@google-cloud/pubsub');

exports.function = function (req, res) {
    const pubsub = PubSub();
    const topic = pubsub.topic(''&lt;TOPIC&gt;');

    topic.publish('Test message', function(err) {
        if (err) {
            res.status(500).send('Error publishing the message: ' + err);
        } else {
            res.status(200).send('1 message published');
        }
    });
};

After Optimization

To remove unnecessary connections and DNS queries, it is enough to create the pubsub object in global scope:

const PubSub = require('@google-cloud/pubsub');
const pubsub = PubSub();

exports.function = function (req, res) {
    const topic = pubsub.topic('');

    topic.publish('Test message', function(err) {
        if (err) {
            res.status(500).send('Error publishing the message: ' + err);
        } else {
            res.status(200).send('1 message published');
        }
    });
};

Testing Your Function

To measure how many connections your function performs on average, simply deploy it as a HTTP function and use a performance-testing framework to invoke it at certain QPS. One possible choice is Artillery, which you can invoke with a single line:

$ artillery quick -d 300 -r 30 <URL>

This command fetches the given URL at 30 QPS for 300 seconds.

After performing the test, check the usage of your connection quota on the Cloud Functions API quota page in Cloud Console. If the usage is consistently around 30 (or its multiple), you are establishing one (or several) connections in every invocation. After you optimize your code, you should see a few (10-30) connections occur only at the beginning of the test.

You can also compare the CPU cost before and after the optimization on the CPU quota plot on the same page.

Monitor your resources on the go

Get the Google Cloud Console app to help you manage your projects.

Send feedback about...

Cloud Functions Documentation