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.
| Constraint | Real value |
|---|---|
| Memory | roughly 128 MB, fixed |
| Timeout | 30 seconds, everything included |
| Node version | Node 22 (as we write this) |
| File system | none, everything lives in memory |
| Browser APIs | absent (no document, window...) |
| Response size | roughly 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 of | Use (native to Node 22) |
|---|---|
uuid | crypto.randomUUID() |
axios (simple requests) | fetch() |
moment | Intl.DateTimeFormat, Date |
buffer | Buffer (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.