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 anythingEvery 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") # GCPThe 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.
