Neon 睡著時,別讓使用者看到 500
客戶專案踩坑
Ricky-
08 月 22,2026

客戶網站的訂閱與發文都走同一個雲端資料庫。上線沒多久,我們注意到一個規律:只要一段時間沒人操作,下一個寫入請求就會卡很久、甚至直接回 500。使用者按下「訂閱」,轉了快一分鐘,最後拿到一個錯誤——這是最不該出現在「轉換動作」上的體驗。

把後端日誌撈出來,畫面大概長這樣:

cannot connect to Postgres: Connection terminated due to connection timeout
Connection terminated unexpectedly
POST /subscribe 500 in 88s   # 使用者就這樣等了 88 秒
現象

只有「閒置後的第一個請求」會中招;重試一次往往就正常了。這是最典型的暫時性故障——偶發、可自癒、卻剛好砸在使用者臉上。

為什麼會這樣

Neon 這類 serverless Postgres 會在閒置後自動暫停來省資源。暫停後的第一個查詢要先把資料庫喚醒、重新建立連線,這段冷啟動可能要數十秒;而連線池裡原本握著的連線這時早已失效,直接拿來用就會噴連線錯誤。

所以這不是 bug,是 serverless 的本質。我們沒辦法消滅冷啟動,但可以讓它對使用者隱形。

t0 ≈ 3.5s 資料庫暫停中 / 喚醒中… 已就緒 試 1 ✗ 逾時 退避 0.5s 試 2 ✗ 連線斷 退避 1s 試 3 ✓ 成功回應 使用者感受:慢一兩秒,但成功——不是 500
一層退避重試,把偶發的冷啟動 500 變成「慢一點但成功」。

解法:一層很薄的重試

我們加了一個包裝函式 withDbRetry(),把「取得連線+這次查詢」整段包起來。重點只有三個:只重試連線類的暫時錯誤、用指數退避給資料庫醒來的時間、以及設一個嘗試上限讓最壞情況的延遲有界。

async function withDbRetry(fn) {
  for (let n = 1; n <= ATTEMPTS; n++) {
    try { return await fn(); }
    catch (err) {
      // 不是連線錯誤、或用完次數 → 立刻拋,不要硬撐
      if (!isTransientDbError(err) || n === ATTEMPTS) throw err;
      await sleep(BASE * 2 ** (n - 1)); // 0.5s → 1s → 2s
    }
  }
}
錯誤發生 連線類 暫時錯誤? 是 退避後重試 0.5s → 1s → 2s(有上限) 否 立刻拋出:email 格式 / unique 衝突 / 任何 4xx
重試只走「暫時性連線錯誤」這條;其餘一律立刻拋,別浪費時間也別送出重複資料。

真正的眉角在這裡

重試很容易寫,但寫錯方向反而更糟。這幾條是我們在客戶站上實際踩過、才補起來的:

  • 只重試「連線」錯誤。email 格式錯、unique 衝突、任何 4xx,都要立刻往上拋。重試它們不但沒用,還可能送出重複資料。
  • 錯誤鏈要穿透。CMS 常把底層的 pg 錯誤包一層再丟出來,判斷是不是暫時性故障時,要往 err.cause / originalError 一路挖進去。
  • 寄信別包進重試。SMTP 本來就慢,把它一起重試只會讓請求更久、更容易再逾時。我們讓寄信留在資料庫重試範圍之外。
  • 冪等要顧。萬一連線是在 commit 之後才斷、重試又跑一次建立,得靠 unique 衝突被吸收成「已訂閱」,而不是變成 500 或重複寄出歡迎信。
帶得走的一句話

Serverless 的冷啟動消滅不了,但可以讓它對使用者隱形。一層只針對暫時性連線錯誤、帶退避與上限的重試,就能把偶發的 500 變成「慢一兩秒但成功」。

這是我們在這個旅遊部落格客戶專案裡,一連串「serverless 很方便、但邊角要自己收」的其中一坑。之後幾篇會接著寫我們在同一個站上遇到的其他問題。

Tags:
NeonPostgres可靠性


相關文章
從零開始製作一個ip查詢網站-第二章 獲取使用者的IP
從零開始製作一個ip查詢網站-第二章 獲取使用者的IP
chris - 2025-11-29T05:54:37Z
想知道自己的 IP 是多少,並為後續地理定位做準備嗎?這篇文章完整教你如何在 React 網頁中抓取使用者公開 IP,說明為什麼瀏覽器無法直接提供真實 IP,並示範使用免費的 ipify API 快速...
從零開始製作一個ip查詢網站-第四章 在頁面中加入Google廣告
從零開始製作一個ip查詢網站-第四章 在頁面中加入Google廣告
chris - 2025-12-28T04:48:22Z
本篇教學將示範如何在 React 專案中加入 Google AdSense 廣告,從取得廣告程式碼、封裝成可重用的元件,到在 IP 查詢網站中正確顯示廣告,兼顧網站體驗與變現需求。
測試要測「真的會發生的那條路」
測試要測「真的會發生的那條路」
ricky - 2026-08-22T09:12:20.021686Z
驗收發文通知時只測了「草稿改發佈」,但使用者走「新建即發佈」——好測的路剛好避開了 404,提醒要測真實路徑。
訂閱電子報
隨時掌握最新的創業故事與設計開發過程
內容包含來自麒航團隊的日常分享、專業知識與工具推薦,助你了解更多創業背後的思維與技術。
麒航私房推薦
Clelereve Blog LogoClelereve Blog LogoClelereve Blog Logo
廣告區
Clelereve Blog Logo
聯絡我們
任何合作或問題請洽:
clelereve@gmail.com
Follow us
Copyright © 2025 blog.clelereve.com - All Rights Reserved
Frontend Version: -- | Backend Version: --
創業實驗筆記 - Clelereve Blog | Neon 睡著時,別讓使用者看到 500 | Clelereve Blog