Fit a Python or Node app into 512 MB.
A shared laptop with a hard memory cap rewards small, predictable services. This guide covers the practical parts: the entrypoint, the port, dependency installs, memory habits and the details that matter when the app is public for a day.
Start with the right shape of app
Mayfly runtime boxes are meant for a single small process. A Python box runs python -u main.py; a Node box runs node index.js. The platform starts the process from the box folder, provides five localhost ports in the environment and creates a temporary public tunnel to the primary port.
That shape fits a small API, a webhook receiver, a demo web server or a scheduled task that also exposes a health page. It does not fit a database cluster, a long-running data pipeline, a GPU workload or anything that assumes it can use several gigabytes of memory. If your app needs those things, a 512 MB shared box is the wrong environment.
The best way to stay under the cap is not to tune aggressively at the end. It is to keep the dependency tree, in-memory data and request concurrency small from the start.
Read the port from the environment
Do not hard-code a port. The host allocates random ports and passes them to the process. The primary port is available as PORT; the others are PORT_2 through PORT_5, and the full list is in MAYFLY_PORTS. The public tunnel targets the primary port.
Node example
const http = require('node:http');
const port = Number(process.env.PORT || 3000);
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
res.end('Mayfly demo is awake.');
});
server.listen(port, '127.0.0.1', () => {
console.log(`listening on 127.0.0.1:${port}`);
});Python example
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
port = int(os.environ.get('PORT', '3000'))
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/health':
body = json.dumps({'ok': True}).encode()
self.send_response(200)
self.send_header('content-type', 'application/json')
self.send_header('content-length', str(len(body)))
self.end_headers()
self.wfile.write(body)
return
body = b'Mayfly demo is awake.'
self.send_response(200)
self.send_header('content-type', 'text/plain; charset=utf-8')
self.send_header('content-length', str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(('127.0.0.1', port), Handler).serve_forever()Binding to 127.0.0.1 is the safe default. The tunnel runs inside the same box sandbox and reaches the local process over loopback. Binding to 0.0.0.0 stays inside the box as well, because the sandbox blocks local-network traffic; there is no reason to use it.
Keep dependencies honest
Mayfly installs dependencies before starting a box when it finds a manifest in the box root:
- Node:
package.jsoninstalls intonode_moduleswith npm. - Python:
requirements.txtinstalls into.deps, which is added toPYTHONPATH.
Installer output appears in the box log. Installed packages count toward the box disk quota. Avoid development dependencies in the manifest, and do not upload a local node_modules folder; let the host install from the manifest. If the app exits because a module is missing, the box page can add the package to the manifest and restart.
A small manifest is also a memory decision. Large frameworks, ORMs and broad utility packages can add tens or hundreds of megabytes before your own code does anything. Check what is actually imported and remove anything that only exists because it was in a tutorial.
Memory habits that actually help
The 512 MB cap is enforced by a watchdog that samples the process tree roughly every 20 seconds. A box that goes over the limit is stopped and marked killed. That is a resource limit, not a security boundary, but it is still worth working with rather than fighting.
These habits make a large difference on a small host:
- Stream files instead of reading them whole. A 20 MB upload should not become a 70 MB buffer plus a copy. Read in chunks or use the framework's streaming response.
- Cap request bodies. Reject oversized JSON and multipart uploads before parsing them. Memory abuse is often accidental.
- Do not keep unbounded arrays or caches. An in-memory list of every request, log line or uploaded item will grow until the watchdog notices.
- Limit concurrency. Ten simultaneous image transformations can use ten times the memory of one. A small queue or a single worker is often enough for a demo.
- Use the filesystem carefully. The box has a disk quota, but every file also has to be read through memory at some point. Stream large content and clean up temporary files.
- Watch the startup baseline. Measure the process after it boots. If a framework already uses most of the budget before serving a request, change the framework, not the limit.
Node may keep a larger heap than you expect unless you tune it. Python web servers can spawn multiple worker processes, each with its own memory. Choose a single worker and a simple runtime for a temporary demo; scale-out is a problem for a different host.
Assume the public URL is public
A runtime box gets its own temporary Cloudflare tunnel. The URL is random, but it is not private. Anyone with the link can reach the process, and the process should behave accordingly.
- Add authentication if the app exposes anything sensitive or destructive.
- Do not expose admin endpoints on a path you hope nobody will guess.
- Do not put secrets in client-side JavaScript or query strings.
- Do not use the box as a store for personal data, credentials or private files.
- Include request timeouts and basic rate limits where possible.
Mayfly runs each runtime box inside a lightweight per-box sandbox. It can read and write only its own folder, run its Node/npm or Python/pip tooling, and reach outbound internet. Host files, other boxes and the local network are blocked. The sandbox is a mitigation, not a proof: if the guard fails the box falls back to unsandboxed with a visible warning. Only upload code you trust, and keep secrets out of boxes. The memory cap limits memory, not behaviour.
Make the logs useful, not noisy
The box log holds the app's stdout and stderr and is cleared whenever the box starts. Print a few important lifecycle messages: the port, a startup confirmation, and one line per failed request. Do not print every successful health check if something is already polling it.
Logs are also how you diagnose a start failure. If the process exits immediately, read the log before changing code. Common causes are a missing dependency, a syntax error, a hard-coded port or a process that exits after starting a background job.
A short pre-flight checklist
- The app listens on
process.env.PORToros.environ['PORT'], not a fixed port. - The entrypoint is named
index.jsormain.pyat the box root, or the start command is updated to match. - The dependency manifest is committed and small enough for the disk quota.
- A health route responds without expensive work.
- Oversized request bodies and unbounded caches are handled.
- No secrets, private data or destructive admin actions are exposed publicly.
- The source code is also stored outside Mayfly; the box is not a backup.
Design for the memory you have, not the memory you might buy later.
One more constraint: the box still disappears after 24 hours. Build the smallest version that can be useful for the window, keep the source elsewhere and let the deadline be part of the plan.