How to Safely Run User Code in a Solo SaaS and Keep Costs Under $50 a Month
Offering a feature that lets users run their own custom scripts directly within your service is very compelling, especially when building AI agents or data automation tools. The catch is security. When you are running the backend all by yourself, uploading arbitrary code from strangers to your server is always spine-chilling. If a container escape exploit happens, every single API key stored in your environment variables gets compromised. On the other hand, spinning up a Kubernetes cluster brings an infrastructure management burden and cost that you simply cannot handle.
In situations like this, the combination of the Vercel Sandbox API and Vercel Blob becomes a practical alternative. Because it uses AWS Firecracker-based MicroVMs, an isolated kernel per session spins up and disappears in just a few hundred milliseconds.
1. Avoiding Idle Resource Billing
If you execute user code directly via eval() or child_process, the entire single process crashes. Keeping Docker containers running continuously is also a heavy burden for a solo developer because server costs continue to accrue even during I/O wait times, such as when a user is waiting for a network request or has added a sleep delay.
Vercel Sandbox only charges for the Active CPU time where actual computation occurs.
| Platform |
Base Monthly Subscription |
CPU Billing Method |
I/O Wait Time Cost |
Max Concurrency Limit |
| Vercel Sandbox (Pro) |
$20 |
Active CPU ($0.128/vCPU-hr) |
$0 |
2,000 sessions |
| E2B (Pro) |
$150 |
Total Execution Time ($0.0504/vCPU-hr) |
Normal billing |
100 sessions |
| CodeSandbox (Scale) |
$170 |
Total Execution Time ($0.1486/hr Nano) |
Normal billing |
250 sessions |
By managing your traffic within the basic credit range of the Pro plan, you can keep expenses under $50 a month without any fixed-cost burden.
2. Sandbox Instance Creation and Forced Resource Reclamation
To prevent infinite loops or memory leaks, you need to bind the instance lifecycle in code. You must specify persistent: false so the virtual machine is immediately discarded upon completion, preventing snapshot storage costs ($0.08 per GB/month) from piling up.
`typescript
import { Sandbox } from '@vercel/sandbox';
interface CodeExecutionRequest {
code: string;
timeoutMs?: number;
}
interface CodeExecutionResult {
stdout: string;
stderr: string;
exitCode: number;
durationMs: number;
}
export async function executeUserCode(
payload: CodeExecutionRequest
): Promise {
const timeout = payload.timeoutMs || 15_000;
const startTime = Date.now();
const sandbox = await Sandbox.create({
timeout,
persistent: false,
runtime: 'node24',
});
try {
await sandbox.writeFiles([
{
path: 'index.js',
content: Buffer.from(payload.code),
},
]);
const commandResult = await sandbox.runCommand('node', ['index.js']);
const stdout = await commandResult.stdout();
const stderr = await commandResult.stderr();
const exitCode = commandResult.exitCode;
return {
stdout,
stderr,
exitCode,
durationMs: Date.now() - startTime,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
stdout: '',
stderr: Execution Failed: ${errorMessage},
exitCode: -1,
durationMs: Date.now() - startTime,
};
} finally {
await sandbox.stop();
}
}
`
If you do not explicitly close sandbox.stop() in the finally block, resources remain tied up until a timeout occurs, causing unnecessary charges.
3. Bypassing Serialization Limits for Large Code Uploads
If you upload code through the frontend via the backend API, you will hit the 4.5MB payload limit of Vercel Serverless Functions. Having the client upload files directly to Vercel Blob Storage removes the backend bottleneck.
`typescript
import { issueSignedToken } from '@vercel/blob';
import { handleUploadPresigned, type HandleUploadPresignedBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server';
async function authenticateRequest(req: Request) {
return { userId: 'user_dev_01', isAuthorized: true };
}
export async function POST(request: Request): Promise {
const body = (await request.json()) as HandleUploadPresignedBody;
try {
const jsonResponse = await handleUploadPresigned({
body,
request,
getSignedToken: async (pathname) => {
const user = await authenticateRequest(request);
if (!user.isAuthorized) {
throw new Error('Unauthorized access');
}
const token = await issueSignedToken({
pathname,
operations: ['put'],
allowedContentTypes: ['text/plain', 'application/javascript', 'application/json', 'application/zip'],
maximumSizeInBytes: 5 * 1024 * 1024,
validUntil: Date.now() + 10 * 60 * 1000,
});
return {
token,
urlOptions: {
addRandomSuffix: true,
allowOverwrite: false,
},
};
},
onUploadCompleted: async ({ blob }) => {
console.log(`Blob storage upload completed: ${blob.url}`);
},
});
return NextResponse.json(jsonResponse);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: message }, { status: 400 });
}
}
`
Vercel Sandbox offers free inbound traffic. When fetching script files uploaded to Blob Storage into the sandbox, no additional network costs are incurred.
4. Log Streaming and Cost Safety Guards
If you do not display execution status to the client in real time, you will be flooded with inquiries saying it looks frozen. By subscribing to the { detached: true } option and the command.logs() async iterator on the backend, you can separate stdout and stderr and pass them directly to the frontend.
`typescript
const command = await sandbox.runCommand({
cmd: 'node',
args: ['index.js'],
detached: true,
});
for await (const line of command.logs()) {
if (line.stream === 'stdout') {
process.stdout.write([STDOUT]: ${line.data});
} else if (line.stream === 'stderr') {
console.error([STDERR]: ${line.data});
}
}
const executionResult = await command.wait();
if (executionResult.exitCode !== 0) {
console.warn(Execution failed with exit code: ${executionResult.exitCode});
}
`
There are several detailed settings to keep in mind during operations:
- Specify the instance deployment region as US East (
iad1, $0.128/hr) or Cleveland (cle1) instead of the default. Computation unit prices are lower than regions like Paris (cdg1, $0.177/hr).
- Apply egress rules to block access to the internal cloud metadata address (
169.254.169.254) to prevent SSRF attacks.
- Set a spending limit around $40 in the Vercel dashboard's Spend Management.
By tying together MicroVM-based isolation and a direct Blob transfer pipeline, you can focus strictly on product features without getting bogged down in infrastructure tasks.