
看過先前的文章,我相信各位可以把一個 bun server 弄起來,我這裡就不掩飾了,我們著重在其他部分壓測
今天的主題主要以 wrk 這個工具為主,當然我個人也有用過 k6 個人覺得相當不錯,壓測的部分讀者可以根據自己
喜歡的部分去嘗試壓測,仿間壓測的部分相當多,並沒有一定要用 wrk 不過我們這裡以 wrk 為主
我們 try try 看 可以撐到多少 QPS 然後再做簡單的優化,當然,因為篇幅有限,我們能做的也是比較明顯的部分
希望讀者能在這篇能得到一些基本的啟發
仿間有非常多壓測工具,比如 ab, autocannon, k6, 還有老字號的 Jmeter, 我們這次嘗試用 wrk
當然既然選擇 wrk 就要說一下他的優點,以及為何選擇
效能足夠高 : 這為何重要呢? 能在單一多核心 CU 上產生相當可觀的負載,結合多執行緒與 epoll, kqueue 等擴展事件通知系統
本身不會成為瓶頸
官方推薦 : 詳細可以看 bun server,bun vs Node.js: Runtime Speed & Install Benchmarks 2026
那大家都用,我們這裡也用.當然人家用一定有他的理由,主要理由是,如果壓測工具本身跑不動,根本量不出 Bun server 的真實極限
支持 Lua script : 可以客製化 request,做更貼近真實流量的測試(POST、JSON body、隨機參數等)
macOS 可以直接用 Homebrew:
brew install wrk
Linux 則需要自己編譯:
git clone --depth=1 https://github.com/wg/wrk.git
cd wrk
make -j
# 編譯完成後的執行檔會在當前目錄下
sudo cp wrk /usr/local/bin/
先寫一支最陽春的 API,之後我們會針對它做優化對照:
index.ts
interface User {
id: number,
name: string
}
Bun.serve({
port: 3000,
fetch(req: Request) {
const url = new URL(req.url);
if (url.pathname === '/ping') {
return new Response('pong');
}
if (url.pathname === 'users') {
const users = Array.from<unknown, User>({ length: 50 }, (_, i) => ({
id: i + 1,
name: `user-${i + 1}`
}));
return Response.json(users, { status: 200 });
}
return new Response('Not found', { status: 404 });
}
});
console.log('run on http://localhost:3000');
啟動:
bun index.ts

wrk 最基本的三個參數:
-t:threads 數量(執行緒)-c:connections 數量(同時保持開啟的連線數)-d:測試持續時間wrk -t4 -c100 -d30s --latency http://localhost:3000/ping
-t4:使用 4 個執行緒-c100:保持 100 個連線-d30s:跑 30 秒--latency:額外印出延遲分布資訊一個典型的輸出長這樣(數字會依你的機器不同):

這裡不需要把執行緒數和連線數設得太大,只要能讓目標程式的 CPU 使用率打到接近 100% 就足夠了;同時測試時間也不能太短,太短的壓測沒有意義
建議搭配 htop 或 top,一邊壓測一邊觀察 CPU 使用率,確保真的把 server 壓榨出極限,而不是壓測工具自己先撐不住。
補充說明 : 如果讀者使用的是 macbook arm 架構 htop 可能會有失準的狀況,我這時候建議用 btop 各位可以自行安排,以下為 btop 的介面

對了,我這裡只是展示 btop 不是壓測當下的樣子😅 言歸正傳,以下
/users)wrk -t4 -c200 -d30s --latency http://localhost:3000/users
因為 /users 多了一個陣列建構 + JSON 序列化的動作,理論上吞吐量會比純文字的 /ping 低一些。這個對照組很重要,等一下優化完可以直接比較差異。

如果要測的是接收 JSON body 的 API,可以寫一個簡單的 Lua script:
-- post.lua
wrk.method = "POST"
wrk.body = '{"name": "ironman", "day": 28}'
wrk.headers["Content-Type"] = "application/json"
然後這樣執行:
wrk -t4 -c100 -d30s -s post.lua --latency http://localhost:3000/echo
(這裡我只是提及,就不截圖展示了)
跑了幾輪之後,常見會發現幾種典型的效能問題:
/users 範例,每次請求都重新 Array.from 產生 50 筆假資料,這種靜態或變動不大的資料,完全可以快取起來console.log,在高並發下 I/O 反而變成瓶頸。-> 只計算一次,之後直接複用
interface User {
id: number,
name: string
}
const cachedUsers = Array.from<unknown, User>({ length: 50 }, (_, i) => ({
id: i + 1,
name: `user-${i + 1}`
}));
const cachedUsersResponse = JSON.stringify(cachedUsers);
Bun.serve({
port: 3000,
fetch(req: Request) {
const url = new URL(req.url);
if (url.pathname === '/ping') {
return new Response('pong');
}
if (url.pathname === 'users') {
return new Response(cachedUsersResponse, {
headers: { "Content-Type": "application/json" },
});
}
return new Response('Not found', { status: 404 });
}
});
console.log('run on http://localhost:3000');
把 Response.json()(內部要做序列化)換成預先算好的字串 + 直接回傳 Response,可以省下每個 request 重複做的序列化成本。
以下結果
與其在 fetch 裡面手動 if/else 判斷 pathname,Bun 原生支援路由物件寫法,內部有針對路由比對做優化,同時程式碼也更乾淨:
Bun.serve({
port: 3000,
routes: {
"/ping": () => new Response("pong"),
"/users": () =>
new Response(cachedUsersResponse, {
headers: { "Content-Type": "application/json" },
}),
},
fetch() {
return new Response("Not Found", { status: 404 });
},
});
確認 handler 裡沒有同步的重運算或同步 I/O。
如果真的有 CPU 密集工作(例如圖片處理、加密運算),考慮丟到 Worker 或用 Bun.spawn 開子行程處理,避免卡住主執行緒的事件迴圈。
// ❌ 每個 request 都同步印出
console.log(`${req.method} ${url.pathname}`);
// ✅ 改成取樣紀錄,或用非同步/批次寫入的 logger
程式碼對比
(順便一提我這裡把 length 改為 1000 因為 50 壓測結果比較相近,
在標準差底下會有點失準,也可能跟我個人的電腦規格較好有關),因此我這裡提供壓測當下的程式碼
before::
interface User {
id: number,
name: string
}
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url)
if (url.pathname === 'ping') {
return new Response('pong');
}
if (url.pathname === '/users') {
const users = Array.from<unknown, User>({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `user-${i + 1}`
}));
return Response.json(users);
}
return new Response('Not found', { status: 404 });
}
});
console.log('run on http://localhost:3000');
after::
interface User {
id: number,
name: string
}
const cachedUsers = Array.from<unknown, User>({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `user-${i + 1}`
}));
const cachedUsersResponse = JSON.stringify(cachedUsers);
Bun.serve({
port: 3000,
routes: {
"/ping": () => new Response("pong"),
"/users": () =>
new Response(cachedUsersResponse, {
headers: { "Content-Type": "application/json" },
}),
},
fetch() {
return new Response('Not found', { status: 404 });
}
});
console.log('run on http://localhost:3000');
用同樣的壓測指令,針對優化前後的 /users 各跑一次,把結果整理成表格會更直觀:
before 結果
after 結果
| 版本 | Req/Sec | p50 延遲 | p99 延遲 |
|---|---|---|---|
| 優化前 | 14,054.84 | 14.14ms | 14.77ms |
| 優化後 | 35,224.37 | 5.65ms | 6.7ms |
再次提醒 :: 以上數字僅為示意,實際結果請以自己機器實測為準——壓測數字受硬體、作業系統、網路環境影響很大,
重點是「同一台機器、優化前後」的相對比較,而不是拿網路上的數字來對標自己的專案。
壓測工具和目標 server 別放同一台機器搶資源:理想情況下 wrk 跟 server 應該在不同機器(或至少不同容器),
否則你量到的可能是壓測工具自己的瓶頸,而不是 server 的瓶頸。
連線數不是越高越好:一開始先從小連線數開始,逐步往上加,找到吞吐量開始下降或延遲開始飆升的臨界點,那才是這台 server 的真實承載上限。
別只看平均值:p99、p999 延遲才是真正決定使用者體驗的關鍵指標。
多測幾輪取穩定值:單次結果容易受到系統當下狀態影響,建議跑 3 次以上取中位數。
今天我們用 wrk 對 Bun server 做了實際的壓力測試,從最基本的用法、如何解讀報告
,到找出瓶頸並實際優化,最後用數字驗證優化是否真的有效。
效能優化最重要的心法就是:先量測,再優化,優化完再量測。沒有數據支撐的優化,往往只是自我感覺良好。