# When does a DigitalOcean Function go cold?

I measured cold starts on DigitalOcean Functions over one morning: 12 functions, 444 calls, idle gaps from 30 seconds to an hour. There was no fixed warm time. A container that had been busy for a few minutes survived idle gaps of up to 15 minutes, while one started seconds earlier was often gone within three.

## How did I tell a cold call from a warm one?

I didn't want to guess from latency, so each function tells me itself. When its code loads, it makes up a random container ID and starts counting calls. A response with a call count of 1 ran in a brand new container.

The rest of the setup:

- Four functions, all web functions in `fra1` with 512 MB of memory:
  - **pysmall**: Python 3.12, no dependencies
  - **nodesmall**: Node 22, no dependencies
  - **nodeheavy**: Node 22 with two AWS SDK v3 clients and lodash
  - **pyheavy**: Python 3.12 with numpy
- Three copies of each, so 12 functions. That let several idle gaps run at the same time without one copy's traffic keeping another warm.
- A Python script on my laptop in Bulgaria, opening a new HTTPS connection for every request. It calls a function, waits, calls it again, then makes two more calls straight away.
- 12 idle gaps from 30 seconds to 60 minutes, shuffled into a different order for each copy, so every gap was tried once on every copy.

It ran for 3 hours 19 minutes. All 444 calls returned HTTP 200, and they were served by 113 different containers.

## How long does a function stay warm?

| Idle time before the call | Still warm |
|---|---|
| 30 s | 11 of 12 |
| 1 min | 10 of 12 |
| 2 min | 9 of 12 |
| 3 min | 6 of 12 |
| 5 min | 6 of 12 |
| 7 min | 4 of 12 |
| 10 min | 4 of 12 |
| 15 min | 2 of 12 |
| 20 min | 0 of 12 |
| 30, 45, 60 min | 0 of 12 each |

Nothing was still warm at 20 minutes. Below that it falls off gradually rather than at one point. One function was cold after 30 seconds, and two were still warm after 15 minutes. With twelve samples a row, I wouldn't make anything of 3 and 5 minutes coming out the same.

## So what decides it?

I expected the spread to be random. Then I checked how old each container was at the moment its idle gap began. For the gaps from 3 to 15 minutes:

| Container age when the idle started | Still warm afterwards |
|---|---|
| under 10 seconds | 9 of 39 |
| 30 seconds to 2 minutes | 2 of 10 |
| 3 minutes or more | 11 of 11 |

A container started for one request and then left alone usually didn't last. One that had been around for three minutes or more survived every gap in that range, 15 minutes included. The oldest one in the run answered its last call 33 minutes after it started.

Most probes in my run had just created a new container, so most containers were young when they went idle. That's why the first table looks gloomy. I didn't design the test around age, so treat this as a pattern in the data rather than a rule. Still, 11 of 11 against 9 of 39 is a big gap, and it's a sensible thing for a platform to do: drop containers nobody came back to, keep the busy ones.

In practice, a function with steady traffic rides out quiet minutes well. The function that gets one request now and then is the one paying for cold starts.

## How much does a cold start cost?

These are measured on my laptop, so they include the network and a fresh TLS handshake each time. TCP plus TLS alone had a median of 33 ms. Warm here means the two calls made straight after another call.

| Function | Cold, median | Cold, range | Warm, median |
|---|---|---|---|
| pysmall | 912 ms | 801 – 1,370 ms | 89 ms |
| nodesmall | 893 ms | 746 – 1,419 ms | 89 ms |
| nodeheavy | 1,817 ms | 1,603 – 2,822 ms | 89 ms |
| pyheavy | 3,314 ms | 2,534 – 7,403 ms | 90 ms |

Without dependencies, Python and Node were the same, about 0.9 seconds cold and 90 ms warm.

## Is it the imports?

Mostly not. Both heavy functions timed their own imports:

- nodeheavy: the AWS SDK and lodash took a median of 148 ms to import, yet the cold start was 924 ms slower than nodesmall
- pyheavy: `import numpy` took a median of 355 ms, yet the cold start was 2.4 seconds slower than pysmall

Most of the extra time passed before my code ran at all, presumably getting the bigger package into the new container. The import time a profiler shows you is the smaller share. The size of what you deploy is the bigger one.

## Anything else odd?

Two things.

The first warm call after an idle gap was slower than the ones after it, a median of 146 to 161 ms depending on the function, against 89 ms for the next two. Same container, same code.

And a call made right after another one can still land in a new container. That happened 9 times out of 288, and in 7 of those the call before had itself been a cold start. One nodeheavy copy got a new container three calls in a row, about a second apart. I don't know why, but it means one cold start can turn into two.

## What I got wrong

My first pyheavy imported numpy and pandas. The remote build failed with `413 Payload Too Large`. Built functions are limited to 48 MB, and the two libraries together were over it, I assume.

With numpy alone at the default 256 MB, every call failed after about 31 seconds with "There was an error processing your request." I blamed the numpy import. A version that didn't import numpy failed the same way, so that wasn't it. Invoking it asynchronously gave me the answer, because async calls get a full activation record: `initTime: 30001` and "The function exhausted its memory and was aborted." It ran out of memory before any of my code ran. At 512 MB it was fine, and I moved all four functions to 512 MB to keep things equal.

I also looked for my web calls in `doctl serverless activations list` and found none. Records are kept for asynchronous invocations, which the docs do say. That's the reason each function reports its own container ID.

And I was hoping for one keep-warm number for the title. There isn't one, at least not one you can see from outside.

## Caveats

One region, one memory size, one morning, 12 samples per gap. Load and time of day could change things. Every latency includes the network from Bulgaria to Frankfurt, and the server and laptop clocks disagreed by about a second, so I couldn't split a cold start into its parts. The container-age result is a correlation in data collected for another question. I didn't measure package sizes; I only know numpy plus pandas was over the limit and numpy alone wasn't.

The whole thing cost nothing. DigitalOcean Functions include 90,000 GiB-seconds a month for free, and 444 calls at 512 MB didn't put a line on my invoice. I deleted the namespace afterwards.

## What would I do with this?

- Don't build around a fixed warm time. Here, 2 minutes idle was usually fine, 15 usually wasn't, and a fresh container could be gone in 3.
- Expect a function with steady use to stay warm far more reliably than one called occasionally.
- Keep the deployed package small. Most of the heavy functions' extra cold start happened before the imports.
- If a function with a big library fails with a generic error, raise its memory before profiling anything.
- When a response tells you nothing, invoke the function asynchronously and read the activation record.

