DEV Community

PranavB6
PranavB6

Posted on

Performance Testing Fastify App Builds & Database Connections

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)