Cloud Run has a rule that sounds reasonable until it ruins your evening: if your container doesn’t get its act together fast enough on startup, it gets a polite SIGTERM and a one-way ticket to the void. Around the 60-second cold-start mark, the platform decides you’ve had your chance.

Our Celery worker kept dying before it could even take the stage. No useful error. Just a worker that booted, blinked, and expired. Classic stage fright.

The autopsy

The culprit wasn’t our code. It was our code’s opening monologue - the module-level imports:

# top of aws_scanner.py
import boto3
import numpy
from azure.mgmt.compute import ComputeManagementClient
# ...the entire cloud SDK family reunion, loaded before we did anything

Every one of those heavyweight SDKs gets imported the instant Python touches the module. boto3, numpy, the azure.mgmt.* clan, google.cloud.* - they all show up at once, each one taking its sweet time. By the time the import party finished unpacking, the cold-start clock had run out and Cloud Run had already sent the SIGTERM.

The worker was being killed during its costume change, before the curtain even rose.

The fix: lazy imports (a.k.a. don’t unpack until you need it)

Move the heavy imports inside the functions that actually use them:

def scan(self):
    import boto3            # imported only when we genuinely scan
    client = boto3.client("ec2")
    ...

Now startup is featherweight. The SDKs only load when a scan actually runs - well after Cloud Run has decided we’re alive and healthy. The worker survives long enough to do its job. Standing ovation.

The sequel nobody asked for: the test patches broke

Of course, moving the imports broke a pile of tests, because the old patches targeted the scanner module:

# used to work, now patches a name that no longer exists there
patch("app.services.aws_scanner.boto3")

When the import is lazy, boto3 isn’t an attribute of the scanner module anymore - it’s summoned fresh inside the function. So you patch the source package instead:

patch("boto3.client")                              # AWS
patch("azure.mgmt.compute.ComputeManagementClient") # Azure
patch("google.cloud.something")                     # GCP

The mock has to wait at the door the function will actually walk through, not the one it used to.

The moral

  • Module-level imports run at import time, not at call time. On a serverless platform with a startup budget, that’s the difference between “deployed” and “deceased.”
  • Heavy SDK? Import it lazily, inside the function that needs it.
  • When you make imports lazy, your test patches have to follow the import to its new home - patch the source package, not the module that re-imports it.

Our worker no longer has stage fright. It just needed to stop trying to memorize the whole script before walking on.


Amit Jethva is the CTO and co-founder of Nuvika Technologies Pvt Ltd, makers of Fintropy, a multi-cloud FinOps platform. Learn more at nuvikatech.com.