Ask
28

Cold start is dominated by imports for things most requests never use, is moving them inside the handler the fix?

A serverless API with about a dozen routes. One of them generates PDFs, one talks to a payment provider, one does image work. The rest are ordinary database reads.

Every request pays for all of it at startup, because everything is imported at the top of the file. The PDF library alone is large.

My instinct is to move those imports inside the handlers that need them, so a plain read does not pay for a library it never touches.

Before I refactor a dozen routes, is that actually how it works? And is there a downside, I have a vague worry that a lazy import just moves the cost onto the first user of that route, which might be worse.

3 answers Share
Report

Answering anonymously, a moderator will review it first.

  • @top_level_await · 3w ago

    Before moving imports, look for work happening at module scope, because that is often bigger than the imports themselves and much easier to fix.

    Things that run when the module loads, whether or not anyone asked:

    Client construction. Database clients, SDK clients, HTTP clients created at the top of a file. Some are cheap; some open connections or read configuration.

    Configuration parsing and validation. A schema validating your whole environment at startup is doing real work on every cold start.

    Anything awaited at module scope. This is the worst case, because the module cannot finish loading until it completes - a network call there is directly in the cold path of every request.

    Large constants built by code, rather than declared. A lookup table computed from a list at import time.

    The fix for most of these is the same shape as your lazy import: build it on first use and keep it, rather than building it at load. A small helper that creates the thing once and returns the cached instance afterwards covers nearly all of it, and it is a much smaller change than restructuring imports.

    That pattern also solves your worry more neatly, because the cost is paid once per instance rather than once per request - the second request to that route is fast again.

    26
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
  • @measure_init_first · 3w ago

    Your instinct is right, your worry is also right, and both are resolvable - but measure before you refactor a dozen routes, because the answer differs a lot by runtime.

    What to measure. Timestamp at the very top of the module, timestamp at the start of the handler, and log the difference on a cold invocation. That gap is your init cost, and it is the number this whole exercise is about. Without it you are guessing at which imports matter.

    What you will usually find: a small number of dependencies account for nearly all of it. Anything with native bindings, anything that builds large lookup tables at import, anything that reads or parses files at module scope. The PDF library is a classic. Nine small imports are usually irrelevant.

    So refactor the two or three that dominate, not all twelve routes.

    On your worry: yes, a lazy import moves the cost to the first request that needs it. Whether that is worse depends entirely on the ratio. If one request in fifty makes a PDF, forty-nine requests get faster and one gets slower: clearly good. If half your traffic hits that route, you have moved the cost around for nothing.

    The rule: lazy-load what is rare and expensive. Eager-load what is common, however expensive.

    30
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
  • @lazy_import_lena · 3w ago

    Two caveats from doing this, because it does not behave identically everywhere.

    On some runtimes it does much less than you expect. If the platform bundles everything and evaluates it up front, a dynamic import may already have been loaded and you save only the evaluation, not the parse. On others the saving is large. This is exactly why the measurement in the first answer matters more than the theory: check the number before and after on one route rather than trusting that it helped.

    Dynamic imports change your error surface. An import failure now happens during a request instead of at startup, so a broken dependency reaches a user as a five hundred rather than as a failed deploy. Worth having a smoke test that touches each lazily-loaded route after deploy, since you have removed the check that used to happen implicitly.

    And the honest framing for your situation: the biggest win is usually not paying for the PDF library at all on the eleven routes that do not make PDFs - which you can also get by splitting the heavy route into its own deployment unit if your platform makes that easy. That is a larger change and it is the cleanest version, because the cold start of the ordinary routes then has nothing to do with the expensive one at all.

    1
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report