The Lambda Bubble doesn’t document

A plugin’s server-side actions run on AWS Lambda. The documentation mentions Node.js and npm modules; it doesn’t tell you what happens at the edges. Here are the figures you only discover by reading the logs.

ConstraintReal value
Memoryroughly 128 MB, fixed
Timeout30 seconds, everything included
Node versionNode 22 (as we write this)
File systemnone, everything lives in memory
Browser APIsabsent (no document, window...)
Response sizeroughly 50 MB

You code with these limits in mind from the start, instead of discovering them one night in production.

The 30-second budget

Your action has 30 seconds to do everything: download an external resource, load its modules, process the data, return the result. The trap: if a heavy module is slow to load on a cold start and an external resource is slow to respond, half the budget is gone before the business logic even begins. You allocate this budget deliberately; you don’t improvise it.

Fewer dependencies, fewer surprises

When you write require("some-module"), Bubble scans your code before execution to decide what to install, and refuses to publish if a dependency (or one of its sub-dependencies) carries a known vulnerability. So every module you add is a risk of being blocked, a bite out of those 128 MB, and a licence to check. The best dependency is the one you don’t add: as of Node 22, a lot of what you need is already built in.

Instead ofUse (native to Node 22)
uuidcrypto.randomUUID()
axios (simple requests)fetch()
momentIntl.DateTimeFormat, Date
bufferBuffer (global)

A detail that trips people up: if you load an ESM module with import(), the parser doesn’t see it. You need a “dead” require() so it installs it anyway.

// The parser only reads require(): this dead require forces the install
if (false) require("my-esm-module");

// ...while the real import runs at runtime
const mod = await import("my-esm-module");

It’s the kind of detail you only learn by having shipped real plugins.

Managing memory under 128 MB

Most plugins never give it a thought: a simple API wrapper fits in 30 to 40 MB. But the moment you process files or load a heavy library, the ceiling becomes the main engineering constraint. Load order matters more than you’d think.

// Download while memory is free, THEN load the heavy module
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
const engine = await import("heavy-library"); // the reverse order risks OOM

At 80% of the ceiling, this order makes the difference between an action that passes and one that crashes.

The type contract: return exactly what you declared

Bubble is strict about the type of every output value. A value declared “boolean” that returns the string "false", or a text field that returns undefined, crashes the action with a baffling error. The fix is the fail() helper (seen in our plugin guide) that returns every output, with the right type, including on the failure path.

A common trap: a checkbox mistakenly declared as “text” shows “yes” or “no” instead of a real boolean. It works well enough, until the code returns a boolean and brings everything down. Always check the type of your fields, especially success.

Coding defensively

On the marketplace, anyone can put anything in any field. The code has to survive any input without exposing an internal error. We sort each field into three levels: hard rejection when the value is fundamentally wrong (we return a clear error), automatic correction when it’s fixable without ambiguity (we correct it silently and log it), tolerance when it’s missing but has an obvious default (we take the default). One golden rule: never adjust a monetary amount upward without saying so. And every error message should answer three questions: what happened, what the user provided, and how to fix it.

Debugging what fails silently

A client-side element fails differently from a server action: there’s no server log, the element “just doesn’t show up”. The browser console is your only window: prefix every console.error with the plugin name so you find them fast. And beware third-party SDKs that catch their own errors and rethrow them as objects with no message: e.message is then undefined, and you log “failure: undefined”, which helps no one.

Our test bench

We don’t theorise: these lessons come from real plugins, shipped and maintained. We design, test and document them on our public demo app. See the lab.


A plugin to build, or one to get back on its feet? Let’s talk.