TLDR;
One-line takeaways
- Supabase vs Local: ~57× slower per request (adds ~61 ms).
- New DB connection each test (Supabase): ~6.6× slower than reusing (adds ~448 ms).
- Build+close app each test: adds about ~19 ms per test (Supabase) and ~16–17 ms per test (local).
It all started with one question: When I'm testing my Fastify app, doesn't it cost a lot of performance to build() and close() the app for every test? Especially since I am opening a sql connection every time it builds.
So with the help I wrote a test (with the help of AI):
describe("Fastify app lifecycle perf", () => {
it("A) build+close per test", async () => {
const t0 = nowMs();
for (let i = 0; i < 10; i++) {
const app = await buildTestApp();
const res = await app.inject({
method: "GET",
url: "/readyz",
});
expect(res.statusCode).toBe(200);
await app.close();
}
const t1 = nowMs();
console.log("A) build+close per test:", (t1 - t0).toFixed(1), "ms");
expect(true).toBe(true);
});
it("B) build once, reuse", async () => {
const app = await buildTestApp();
const t0 = nowMs();
for (let i = 0; i < 10; i++) {
const res = await app.inject({
method: "GET",
url: "/readyz",
});
expect(res.statusCode).toBe(200);
}
const t1 = nowMs();
console.log("B) build once, reuse:", (t1 - t0).toFixed(1), "ms");
await app.close();
expect(true).toBe(true);
});
});
Here were the results:
GET /readyz (sql: SELECT 1 as ok)
10x
A) build+close per test: 825.4 ms
B) build once, reuse: 13.0 ms
After that I decided to test its speed when connecting to a real remote database (Supabase in this case) as well as comparing when creating a new connection each time I build vs reusing connections. Here is the culmination of all the results:
Results
When connecting to Supabase
GET /readyz (sql: SELECT 1 as ok)
10x
A) build+close per test: 5122.7 ms
B) build once, reuse: 938.0 ms
GET /readyz (sql: SELECT 1 as ok)
100x
A) build+close per test: 8082.5 ms
B) build once, reuse: 6308.7 ms
POST /api/tickets (creating a ticket)
100x
A) build+close per test: 52949.5 ms
B) build once, reuse: 6582.6 ms
POST /api/tickets (creating a ticket)
100x
REUSING CONNECTION
A) build+close per test: 8068.1 ms
B) build once, reuse: 6192.9 ms
When connecting to local database (container)
GET /readyz (sql: SELECT 1 as ok)
10x
A) build+close per test: 825.4 ms
B) build once, reuse: 13.0 ms
GET /readyz (sql: SELECT 1 as ok)
100x
A) build+close per test: 1486.6 ms
B) build once, reuse: 41.5 ms
POST /api/tickets (creating a ticket)
100x
A) build+close per test: 2586.9 ms
B) build once, reuse: 117.0 ms
POST /api/tickets (creating a ticket)
100x
REUSING CONNECTION
A) build+close per test: 1768.0 ms
B) build once, reuse: 108.2 ms
Understanding the Results
Baseline per-request times (POST /api/tickets)
Local (best case)
- Reuse app + reuse connection (B): ~1.08 ms/request
Supabase (best case)
- Reuse app + reuse connection (B): ~61.9 ms/request
✅ Supabase vs Local adds: 61.9 / 1.08 ≈ ~57× slower per request (about +60.8 ms each)
Reusing connection vs creating a new one each time
We compare A build+close (where connection churn matters most):
Supabase
- New connection each time (A): ~529 ms/request
- Reuse connection (A): ~80.7 ms/request
✅ New connection vs reused adds: 529 / 80.7 ≈ ~6.6× slower (about +448 ms each)
Local
- New connection each time (A): ~25.9 ms/request
- Reuse connection (A): ~17.7 ms/request
✅ New connection vs reused adds: 25.9 / 17.7 ≈ ~1.46× slower (about +8.2 ms each)
Building/closing app each time vs building once and reusing
Compare A vs B with reused connection (so you’re measuring mostly app lifecycle overhead):
Supabase (reusing connection)
- Build+close each request (A): ~80.7 ms/request
- Build once, reuse (B): ~61.9 ms/request
✅ Build+close vs reuse adds: 80.7 / 61.9 ≈ ~1.30× slower (about +18.8 ms each)
Local (reusing connection)
- Build+close each request (A): ~17.7 ms/request
- Build once, reuse (B): ~1.08 ms/request
✅ Build+close vs reuse adds: 17.7 / 1.08 ≈ ~16.4× slower (about +16.6 ms each)
(That “16×” looks huge because local B is insanely fast; the absolute cost is still ~16–17 ms per request.)
One-line takeaways
- Supabase vs Local: ~57× slower per request (adds ~61 ms).
- New DB connection each test (Supabase): ~6.6× slower than reusing (adds ~448 ms).
- Build+close app each test: adds about ~19 ms per test (Supabase) and ~16–17 ms per test (local).
Full Example
import { buildApp } from "../../src/app.js";
async function buildTestApp() {
const app = buildApp({
logger: false,
});
await app.ready();
return app;
}
function nowMs() {
const [s, ns] = process.hrtime();
return s * 1000 + ns / 1e6;
}
describe("Fastify app lifecycle perf", () => {
it("A) build+close per test", async () => {
const t0 = nowMs();
for (let i = 0; i < 100; i++) {
const app = await buildTestApp();
const res = await app.inject({
method: "POST",
url: "/api/tickets",
body: {
title: "Test ticket",
description: "Test description",
priority: 3,
}
});
expect(res.statusCode).toBe(201);
await app.close();
}
const t1 = nowMs();
console.log("A) build+close per test:", (t1 - t0).toFixed(1), "ms");
expect(true).toBe(true);
});
it("B) build once, reuse", async () => {
const app = await buildTestApp();
const t0 = nowMs();
for (let i = 0; i < 100; i++) {
const res = await app.inject({
method: "POST",
url: "/api/tickets",
body: {
title: "Test ticket",
description: "Test description",
priority: 3,
}
});
expect(res.statusCode).toBe(201);
}
const t1 = nowMs();
console.log("B) build once, reuse:", (t1 - t0).toFixed(1), "ms");
await app.close();
expect(true).toBe(true);
});
});
Top comments (0)