Sitemap

GCP Cloud Run VS Cloud Function Cold Starts

5 min readMay 13, 2019

--

Press enter or click to view image in full size
Source: Google Cloud Platform

I’m working on a small side project, for which I’m using Firebase and Google Cloud Functions. So far it’s been great however one of the use cases requires a response from the Cloud Function within a certain time limit. Occasionally, usually after the weekend, the Cloud Function will need to cold start and will take just too long. ‘Cold Starts’ are when a ‘serverless’ service is not cached by the cloud provider and the execution environment needs to be initialized from scratch.

I followed all the tips and tricks recommended by Google in their documentation, however it was never quite quick enough. So I decided to give Google’s new “Cloud Run” a look to see if it could deal with cold starts any better. Cloud Run is Googles new ‘serverless’ container offering. To test this I created a basic Cloud Function and two basic Cloud Run containers, then sent a few requests to them and timed the response.

Cloud Function

For the Cloud Function I simply created a brand new Cloud Function, removed the code that read the request and simply got it to output ‘hello world’. The code looks like this:

exports.helloWorld = (req, res) => {
let message = 'Hello World!';
res.status(200).send(message);
};

Cloud Run

For Cloud Run I created two containers. Both of them are based on the examples in the GCP Documenation. The first container is a Nodejs container, the Dockerfile is almost exactly like the one in the docs, except I changed the runtime to Nodejs 8, to bring it more in line with the Cloud Function runtime. So it looks like this:

FROM node:8WORKDIR /usr/src/app
COPY package.json ./
RUN npm install --only=production
COPY ./ .
CMD [ "npm", "start" ]

The index.js itself looks like this:

const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send(`Hello World`);
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log('Hello world listening on port', port);
});

The package.json file was copied straight from the docs. The Nodejs container was chosen was so it was as close to the cloud function environment as I could get in Cloud Run, so I could get a better comparison. However the resulting docker image ~900mb due to it using the Nodejs base image. Since the image was so large I thought I’d try compare it to a smaller one, to see if that made a difference. To do this I used a Go binary running in a ‘scratch’ container. The Dockerfile is based on the GCP docs, but I changed the second stage to use a ‘scratch’, so it looks like this:

FROM golang:alpine as builderRUN apk update && apk upgrade && apk add --no-cache git
RUN mkdir -p /go/src/github.com/puppetlabs/cloudrun
WORKDIR /go/src/github.com/puppetlabs/cloudrun
ADD ./ /go/src/github.com/puppetlabs/cloudrun
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o ./bin/cloudrun ./...# Second Stage
FROM scratch
WORKDIR /app
COPY --from=builder /go/src/github.com/puppetlabs/cloudrun/bin/cloudrun /app/
CMD ["./cloudrun"]

The Go code itself is just a slightly modified version of the one in the docs:

package mainimport (
"fmt"
"log"
"net/http"
"os"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World\n")
}
func main() {
http.HandleFunc("/", handler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

This resulted in a container that was ~8mb, so an order of magnitude smaller than the Nodejs container, so should provide a good comparison.

Deployment Summary

Press enter or click to view image in full size

The Test

The Cloud Function was deployed during creation. I then deployed the two containers to Cloud Run. Once all was ready to go I got the URLs for all 3 and made a python script to call each URL twice and to measure the response time. So the first call would get the ‘cold start’ time and the second call would get the ‘hot start’ time. I then repeated this a few times in 5 hour intervals to get an average. (5 Hours was to ensure they would no longer be cached)

The Results

The results are quite interesting. First off it appears that Cloud Run Containers have a much shorter cold start than Cloud Functions. So a Cloud Run container can start from cold in half the time a Cloud Function can. I thought they would be comparable, with Cloud Run maybe being a bit slower.

Secondly hot start for both Cloud Functions and Cloud Run seems comparable, with only a few milliseconds in it. This is about what I expected.

Lastly the difference between the Go container at 8mb and the Nodejs container at 900mb are almost identical. So it appears container size has almost no effect on the speed of the cold start. I thought the size of the container would play into how quickly it could be loaded up, but it appears to have little to no effect at all.

What I will perhaps test in future is more complex code. Something that has to load in a few libaries, so we can see if that plays much of a role in the speed of the cold start. Perhaps Go will fare better in this, as it is a self contained binary.

Conclusion

Cloud Run seems to have far faster cold starts than Cloud Functions, at least in this simple example. Container size doesn’t seem to affect the Cloud Run cold starts much etiher. However more investigation will be needed to see how more complex code effects this.

--

--

Andrew Hayes
Andrew Hayes

Written by Andrew Hayes

Staff Software Engineer @ Harness Belfast