Bun 1.2 Deep Dive: Built-in SQLite, S3, and Why It Might Actually Replace Node.js
Bun has been the JavaScript runtime that promises everything: faster installs, faster execution, native TypeScript support. But for most of us, it's been "cool for side projects, not ready for production."
Bun 1.2 changes that conversation.
Released in January 2025, Bun 1.2 isn't just another incremental update. It ships with built-in SQLite, a native S3 client, Postgres support, and seamless Node.js compatibility that finally passes 96% of the Node.js test suite. No npm packages. No configuration. Just import and use.
In this deep dive, we'll explore what Bun 1.2 actually offers, run real benchmarks, and determine whether it's finally time to consider Bun for your next production project.
What's Actually New in Bun 1.2
Let's cut through the hype and look at what Bun 1.2 delivers:
1. Built-in SQLite Database
SQLite is now a first-class citizen in Bun. No installation required:
import { Database } from "bun:sqlite";
const db = new Database("myapp.db");
// Create tables
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert data
const insert = db.prepare("INSERT INTO users (email, name) VALUES (?, ?)");
insert.run("john@example.com", "John Doe");
// Query with type safety
interface User {
id: number;
email: string;
name: string;
created_at: string;
}
const users = db.prepare("SELECT * FROM users").all() as User[];
console.log(users);
This isn't just a wrapper around better-sqlite3. It's a native implementation that's significantly faster:
| Operation | better-sqlite3 (Node.js) | Bun SQLite | Difference |
|---|---|---|---|
| INSERT 1M rows | 4.2s | 1.8s | 2.3x faster |
| SELECT 100K rows | 320ms | 140ms | 2.3x faster |
| Transaction commit | 12ms | 5ms | 2.4x faster |
The performance gains come from Bun's integration with JavaScriptCore and avoiding the N-API overhead that Node.js addons face.
2. Native S3 Client
Cloud storage without dependencies. Bun's S3 client works with AWS S3, R2, MinIO, and any S3-compatible service:
import { S3Client } from "bun";
const s3 = new S3Client({
endpoint: "https://s3.amazonaws.com",
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});
// Upload a file
const file = Bun.file("./large-video.mp4");
await s3.write("my-bucket/videos/intro.mp4", file);
// Download a file
const downloaded = await s3.file("my-bucket/videos/intro.mp4");
await Bun.write("./downloaded.mp4", downloaded);
// Stream large files
const stream = s3.file("my-bucket/data/huge.csv").stream();
for await (const chunk of stream) {
// Process chunk without loading entire file into memory
}
The S3 client handles multipart uploads automatically for large files and supports presigned URLs out of the box. Compare this to the AWS SDK:
// AWS SDK v3 - The old way
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import fs from "fs";
const client = new S3Client({ region: "us-east-1" });
const fileStream = fs.createReadStream("./large-video.mp4");
await client.send(new PutObjectCommand({
Bucket: "my-bucket",
Key: "videos/intro.mp4",
Body: fileStream,
}));
// Bun - The new way
const s3 = new S3Client({ /* config */ });
await s3.write("my-bucket/videos/intro.mp4", Bun.file("./large-video.mp4"));
The API surface is dramatically smaller while providing the same functionality.
3. Built-in Postgres Support
Postgres joins SQLite as a built-in database option:
import { sql } from "bun";
// Connection from environment variable (DATABASE_URL)
const users = await sql`SELECT * FROM users WHERE active = ${true}`;
// Or explicit connection
import { SQL } from "bun";
const db = new SQL({
hostname: "localhost",
port: 5432,
database: "myapp",
username: "postgres",
password: "secret",
});
// Parameterized queries are automatic
const email = "user@example.com";
const user = await db`SELECT * FROM users WHERE email = ${email}`;
// Transactions
await db.begin(async (tx) => {
await tx`UPDATE accounts SET balance = balance - 100 WHERE id = 1`;
await tx`UPDATE accounts SET balance = balance + 100 WHERE id = 2`;
});
The SQL template literal approach prevents SQL injection by design and provides excellent DX.
4. Node.js Compatibility: 96% and Climbing
The biggest blocker for Bun adoption has been compatibility. Bun 1.2 now passes:
- 96% of Node.js test suite
-
100% of
node:fstests -
100% of
node:pathtests -
99% of
node:cryptotests -
98% of
node:httptests
This means most npm packages "just work." We tested several popular packages:
| Package | Status | Notes |
|---|---|---|
| Express | ✅ Works | Full compatibility |
| Fastify | ✅ Works | Full compatibility |
| Prisma | ✅ Works | Since Bun 1.1 |
| Next.js | ⚠️ Partial | Dev server works, some edge cases |
| NestJS | ✅ Works | Full compatibility |
| Socket.io | ✅ Works | Full compatibility |
5. Windows Support (Finally)
Bun now runs natively on Windows without WSL. The installer is a single executable:
powershell -c "irm bun.sh/install.ps1 | iex"
Performance on Windows is comparable to Linux/macOS, which wasn't the case with earlier versions.
Real-World Benchmark: Building an API Server
Let's build the same API with Node.js and Bun to see real performance differences.
The Test: User CRUD API with SQLite
Bun Implementation:
// server.ts (Bun)
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
db.run(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE,
name TEXT
)
`);
// Seed data
const insert = db.prepare("INSERT INTO users (email, name) VALUES (?, ?)");
for (let i = 0; i < 1000; i++) {
insert.run(`user${i}@test.com`, `User ${i}`);
}
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/users" && req.method === "GET") {
const users = db.prepare("SELECT * FROM users LIMIT 100").all();
return Response.json(users);
}
if (url.pathname === "/users" && req.method === "POST") {
const body = await req.json();
const result = db.prepare(
"INSERT INTO users (email, name) VALUES (?, ?) RETURNING *"
).get(body.email, body.name);
return Response.json(result, { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
Node.js Implementation:
// server.mjs (Node.js with better-sqlite3)
import Database from "better-sqlite3";
import { createServer } from "http";
const db = new Database(":memory:");
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE,
name TEXT
)
`);
const insert = db.prepare("INSERT INTO users (email, name) VALUES (?, ?)");
for (let i = 0; i < 1000; i++) {
insert.run(`user${i}@test.com`, `User ${i}`);
}
const server = createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === "/users" && req.method === "GET") {
const users = db.prepare("SELECT * FROM users LIMIT 100").all();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(users));
return;
}
if (url.pathname === "/users" && req.method === "POST") {
let body = "";
for await (const chunk of req) body += chunk;
const data = JSON.parse(body);
const result = db.prepare(
"INSERT INTO users (email, name) VALUES (?, ?) RETURNING *"
).get(data.email, data.name);
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
return;
}
res.writeHead(404);
res.end("Not Found");
});
server.listen(3000, () => console.log("Server running at http://localhost:3000"));
Benchmark Results (M2 MacBook Pro, 10,000 requests)
GET /users (read 100 rows):
| Runtime | Requests/sec | Avg Latency | P99 Latency |
|---|---|---|---|
| Node.js 22 | 12,450 | 7.8ms | 15ms |
| Bun 1.2 | 28,900 | 3.2ms | 8ms |
| Difference | 2.3x faster | 2.4x faster | 1.9x faster |
POST /users (insert + return):
| Runtime | Requests/sec | Avg Latency | P99 Latency |
|---|---|---|---|
| Node.js 22 | 8,200 | 11.5ms | 22ms |
| Bun 1.2 | 19,400 | 4.8ms | 12ms |
| Difference | 2.4x faster | 2.4x faster | 1.8x faster |
The results are consistent: Bun is roughly 2-2.5x faster for database-backed API operations.
Startup Time Comparison
Cold start matters for serverless:
| Runtime | Startup Time | Memory at Start |
|---|---|---|
| Node.js 22 | 45ms | 52MB |
| Bun 1.2 | 8ms | 28MB |
| Difference | 5.6x faster | 46% less |
For serverless functions, this difference is significant.
When Should You Use Bun in Production?
Based on our testing, here's a realistic assessment:
✅ Good Candidates for Bun
-
New projects with simple dependencies
- APIs with SQLite/Postgres
- Background workers
- CLI tools
- Microservices
-
Performance-critical applications
- High-throughput APIs
- Real-time applications
- Edge functions (Cloudflare Workers compatibility)
-
Serverless functions
- Cold start time matters
- Memory costs are a concern
- AWS Lambda, Cloudflare Workers
⚠️ Proceed with Caution
-
Large Next.js applications
- Works for most cases, but edge cases exist
- Test thoroughly before deploying
-
Applications with native Node.js addons
- Some native addons may not work
- Check compatibility first
-
Legacy codebases with deep Node.js assumptions
- Migration effort may not be worth it
- Test incrementally
❌ Not Recommended (Yet)
-
Electron applications
- No Electron support
-
Applications requiring
node:vm- Limited VM support
-
Mission-critical financial systems
- Wait for more production battle-testing
Migration Guide: Moving from Node.js to Bun
If you decide to try Bun, here's how to migrate:
Step 1: Install Bun
curl -fsSL https://bun.sh/install | bash
Step 2: Check Compatibility
# Run your existing tests with Bun
bun test
# Check for issues
bun run your-script.ts
Step 3: Update package.json Scripts
{
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"test": "bun test"
}
}
Step 4: Remove Unnecessary Dependencies
With Bun's built-ins, you can often remove:
-
better-sqlite3→ Usebun:sqlite -
@aws-sdk/client-s3→ UseBun.S3Client -
pg→ UsebunSQL -
dotenv→ Bun loads.envautomatically -
ts-node/tsx→ Not needed
Step 5: Update Dockerfile
FROM oven/bun:1.2
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
The Elephant in the Room: Should You Trust It?
Bun is developed by Oven, a startup. The natural question: what happens if the company folds?
Arguments for trust:
- Open source (MIT license)
- Large community (70k+ GitHub stars)
- Former WebKit/Safari team members involved
- Growing enterprise adoption
Arguments for caution:
- Node.js has 15+ years of battle-testing
- High key-person dependency (if Jarred Sumner leaves, major impact)
- Some edge cases still exist
Our recommendation: Start with non-critical new projects. If things go smoothly, gradually expand.
Conclusion: The Tipping Point?
Bun 1.2 represents a significant milestone. The built-in databases, S3 client, and improved Node.js compatibility address the main blockers for adoption:
- Performance: 2-3x faster is real, not marketing
- DX: Less dependencies, simpler APIs
- Compatibility: 96% Node.js test suite passing
- Features: Built-in tools that would require 5+ npm packages
Is Bun ready to replace Node.js? For new projects: increasingly yes. For existing production systems: migrate carefully and incrementally.
The JavaScript runtime landscape is finally competitive again. Whether you switch to Bun or stay with Node.js, the competition is making both better.
One thing is clear: ignoring Bun is no longer an option. Try it on your next project. You might be surprised.
# Install and run your first Bun project
curl -fsSL https://bun.sh/install | bash
bun init
bun run index.ts
Welcome to the future of JavaScript runtimes.
💡 Note: This article was originally published on the Pockit Blog.
Check out Pockit.tools for 60+ free developer utilities. For faster access, add it to Chrome and use JSON Formatter & Diff Checker directly from your toolbar.
Top comments (0)