Edge Functions คืออะไร
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Edge Functions คือฟังก์ชัน TypeScript ฝั่งเซิร์ฟเวอร์ที่รันบน Deno ถูก deploy กระจายไปทั่วโลกใกล้ผู้ใช้ ใช้ตอนที่ logic ของคุณไม่ใช่แค่ database query
ไม่ใช่ Node.js และไม่ใช่ database query
หัวข้อที่มีชื่อว่า “ไม่ใช่ Node.js และไม่ใช่ database query”ทุก layer ที่คอร์สนี้พูดถึงมาก่อนหน้านี้อยู่ใกล้ Postgres มาก ๆ PostgREST แปลง table ให้กลายเป็น REST API, RPC function รัน logic ที่ใกล้เคียง SQL อยู่ในตัวฐานข้อมูล ส่วน RLS บังคับสิทธิ์ระดับ row ส่วน Edge Functions คือ layer สำหรับทุกอย่างที่ไม่เข้ากับรูปแบบนั้น เช่น เรียก third-party API, เก็บ secret ที่ไม่ควรอยู่ในฐานข้อมูล, ทำงานที่กิน CPU หนักนอกเหนือ SQL หรือทำ HTTP endpoint แบบ stateless ที่ควบคุม request/response เองทั้งหมด
รายละเอียด runtime ที่สำคัญคือ Edge Functions รันบน Deno ไม่ใช่ Node.js Deno มี Deno.serve เป็น global function สำหรับจัดการ HTTP request มาให้ในตัว รองรับ TypeScript แบบ first-class โดยไม่ต้องมี build step และปลอดภัยแบบ secure-by-default ด้วย sandboxing ถ้าคุณเคยเขียน server code บน Node.js มาก่อน รูปแบบ request/response จะคุ้นเคย แต่ API และ permission model เป็นของ Deno เอง
Deno.serve((req) => { return new Response('Hello from the edge')})เลือก Edge Function แทน Postgres RPC function เมื่อไหร่
หัวข้อที่มีชื่อว่า “เลือก Edge Function แทน Postgres RPC function เมื่อไหร่”โมดูล Auto-Generated API ให้วิธีรัน logic เองใกล้ข้อมูลไปแล้วคือ Postgres RPC function นั่นยังคงเป็นตัวเลือกที่ถูกต้องเมื่อ logic นั้นเป็น database operation โดยพื้นฐาน เขียนด้วย SQL ได้ดีที่สุด หรือต้องรันอยู่ใน transaction เดียวกับการเขียนข้อมูลอื่น ๆ ให้เลือก Edge Function แทนเมื่อข้อใดข้อหนึ่งต่อไปนี้เป็นจริง
- ต้องเรียก third-party API ภายนอก (payment provider, email service, AI model)
- logic ต้องใช้ secret (API key, signing key) ที่ห้ามให้ฐานข้อมูลหรือ client อ่านได้
- งานนั้นกิน CPU หนักในแบบที่ SQL แสดงออกได้ไม่สะอาด (image processing, parse string ซับซ้อน, เรียก library อื่น)
- ต้องการ HTTP endpoint แบบ stateless ที่ควบคุม header, status code และ request/response cycle เองเต็มรูปแบบ
ถ้าไม่เข้าเงื่อนไขข้างต้นเลยและ operation นั้นแค่ “อ่านหรือเขียนบาง row พร้อม logic บางอย่าง” RPC function มักจะง่ายกว่าและเร็วกว่าเพราะไม่ต้องออกจากฐานข้อมูลเลย — ดู Database Functions as RPC ว่าทำไม operation ที่ atomic อยู่ในฐานข้อมูลถึงมักเป็นตัวเลือกที่ดีกว่า
workflow ผ่าน CLI
หัวข้อที่มีชื่อว่า “workflow ผ่าน CLI”Supabase CLI มองว่า Edge Functions เป็นแค่ไฟล์ธรรมดาในโปรเจกต์ deploy ได้ด้วยคำสั่งเดียว
# Scaffold a new function at supabase/functions/hello-world/index.tssupabase functions new hello-world
# Run all functions locally for developmentsupabase functions serve
# Ship a function to your live projectsupabase functions deploy hello-worldsupabase functions new สร้างไฟล์และโฟลเดอร์ boilerplate ให้ supabase functions serve เปิด Deno runtime ในเครื่องเพื่อให้ทดสอบได้โดยไม่ต้อง deploy supabase functions deploy อัปโหลด function ให้ infrastructure ฝั่ง edge ของ Supabase รันใกล้จุดที่ request มาที่สุด
flowchart LR
client["Client app"] -->|"GET/POST via PostgREST"| rest["Auto-generated REST API"]
client -->|"rpc()"| rpcfn["Postgres RPC function"]
client -->|"functions.invoke()"| edge["Edge Function (Deno, deployed at the edge)"]
rest --> db[("Postgres")]
rpcfn --> db
edge -->|"optional"| db
edge -->|"optional"| ext["Third-party API"] สิ่งที่รันจริงเมื่อคุณ serve: isolate model
หัวข้อที่มีชื่อว่า “สิ่งที่รันจริงเมื่อคุณ serve: isolate model”supabase functions serve และแพลตฟอร์มโฮสต์ของ Supabase ทั้งคู่ใช้ Edge Runtime ตัวเดียวกัน (github.com/supabase/edge-runtime) ซึ่ง README ของโปรเจกต์เองบอกว่าเป็น “a server based on Deno runtime, capable of running JavaScript, TypeScript, and WASM services” สร้างมาให้ engine ตัวเดียวกันใช้ “to locally test and self-host Supabase’s Edge Functions (or any Deno Function)” ได้ ข้างในมี runtime สองตัว ไม่ใช่ตัวเดียว
- main runtime — คอย proxy request ที่เข้ามา ไม่มี limit ที่ผู้ใช้เห็น และอ่าน environment variable ได้ทุกตัว
- user runtime — Deno isolate แยกต่างหากต่อ function หนึ่งตัว ที่รันโค้ดของคุณจริง ๆ มี memory/time limit บังคับ และเข้าถึง environment variable ได้เฉพาะที่ main runtime อนุญาตให้เท่านั้น
การ sandbox นี้ไม่ใช่แค่ทฤษฎี พิสูจน์จาก local stack นี้จริง: function ที่ใช้ postgres npm driver ต่อไปยัง database pooler ด้วย Docker network hostname (supabase_pooler_probe) พังตอนอยู่ใน user runtime ทั้งที่ hostname เดียวกันนี้ resolve ได้ปกติจากการ docker exec เข้า container เดียวกัน
# from inside the running edge-runtime container directly (docker exec):$ getent hosts supabase_pooler_probe172.18.0.14 supabase_pooler_probe
# from inside a function running in that container's user-runtime isolate:[Error] Error: getaddrinfo ENOTFOUND supabase_pooler_probe at ... postgres/3.4.9/src/query.js:36:24 at Object.handler (file:///var/tmp/sb-compile-edge-runtime/edge-notes-pooler/index.ts:7:25) { code: "ENOTFOUND", syscall: "getaddrinfo", hostname: "supabase_pooler_probe"}function เดียวกันนี้ต่อฐานข้อมูลได้ปกติผ่าน raw IP หรือ host.docker.internal (ใช้ในบทถัดไป) — มีแค่ Docker-internal DNS name ของ container เอง เท่านั้นที่เข้าไม่ถึงจากใน isolate นี่คือหลักฐานที่สังเกตเห็นได้จริงว่า user-runtime sandbox จำกัดสิ่งที่ container “ชั้นนอก” ทำได้ตามปกติ ตรงตามจุดประสงค์ของ two-runtime model เลย รายละเอียด lifecycle แบบเต็ม ๆ ทั้ง permission model, module graph caching และวิธี isolate ถูก retire/แทนที่ อยู่ใน Edge Runtime Isolates
Cold start วัดจริงบน local stack นี้
หัวข้อที่มีชื่อว่า “Cold start วัดจริงบน local stack นี้”isolate ต้อง boot ก่อนถึงจะรับ request แรกได้ request ถัดไปที่เข้า isolate ตัวเดิมที่ยัง warm อยู่จะไม่ต้องเสียค่านั้นซ้ำ พิสูจน์ด้วย curl -w จับเวลา function Deno.serve “hello world” เปล่า ๆ ที่รันผ่าน supabase functions serve บน Mac เครื่องนี้
curl -s -o /dev/null -w "time_total=%{time_total}s http=%{http_code}\n" \ "$FUNCTIONS_URL/edge-hello" -H "Authorization: Bearer $ANON_KEY" -H "apikey: $ANON_KEY"first request (cold isolate): time_total=0.066398s http=200second request (warm isolate): time_total=0.007159s http=200third request (warm isolate): time_total=0.006022s http=200ต่างกันประมาณ 10 เท่า ระหว่าง request แรก (ที่ต้องจ่ายค่า boot isolate) กับ request หลัง ๆ บน isolate ที่ warm แล้ว — เป็นตัวเลขจริงจาก Mac เครื่องนี้ ไม่ใช่ benchmark จากแพลตฟอร์มโฮสต์จริง ตัวเลขที่แน่นอนบน edge network จริงของ Supabase จะต่างออกไป แต่รูปแบบ cold-vs-warm เป็นกลไกเดียวกับที่อธิบายไว้ข้างบน คือ user-runtime isolate ใหม่ต้องถูกสร้างก่อนรับ request แรก และ policy ต่อ worker (policy = "per_worker" ใน config.toml ของ local stack นี้) จะเก็บมันไว้ให้ตอบ request ถัดไปได้
รันที่ไหนบ้าง: serve ในเครื่อง vs. บนโฮสต์จริง
หัวข้อที่มีชื่อว่า “รันที่ไหนบ้าง: serve ในเครื่อง vs. บนโฮสต์จริง”supabase functions serve รัน Edge Runtime binary ตัวเดียวกันเป๊ะกับที่แพลตฟอร์มโฮสต์ของ Supabase ใช้ แค่รันบนเครื่องคุณเอง ชนกับ Postgres ในเครื่อง นี่คือเหตุผลที่การพิสูจน์ในโมดูลนี้ย้ายไปใช้กับ production ได้ตรง ๆ ความต่างที่มีจริงคือเรื่อง ตำแหน่ง และ scale ไม่ใช่ code path ที่จัดการ request
- ในเครื่อง: Edge Runtime instance เดียว โปรเจกต์เดียว secret มาจาก
--env-filehot-reload เมื่อไฟล์เปลี่ยน (policy = "per_worker") ไม่มีการกระจายตามภูมิศาสตร์ - บนโฮสต์จริง: function ถูก deploy ไปยัง global edge network ของ Supabase รันใกล้ region ที่ request มาที่สุด secret มาจาก
supabase secrets setisolate scale แบบ horizontal ข้าม region ไม่ใช่อยู่ใน process เดียวบนโน้ตบุ๊กคุณ
Limits (จากเอกสาร)
หัวข้อที่มีชื่อว่า “Limits (จากเอกสาร)”Supabase เผยแพร่ resource limit ที่ตายตัวสำหรับ hosted runtime (supabase.com/docs/guides/functions/limits)
Maximum Memory: 256MBMaximum Duration (wall clock): Free plan 150s, Paid plans 400sMaximum CPU Time: 2s of actual CPU time per request (excludes async I/O wait)Request idle timeout: 150s (no response before this returns a 504 Gateway Timeout)CPU-time limit อ่านผิดได้ง่าย มันคือ 2 วินาทีที่ CPU คำนวณจริง ๆ ไม่ใช่ 2 วินาที wall-clock — await fetch(...) ไปเรียก third-party API ที่ช้า ไม่กิน CPU time ระหว่างรอ ดังนั้น function รันได้นานกว่า 2 วินาทีได้จริงตราบใดที่ไม่ได้ คำนวณ เกิน 2 วินาทีนั้น นี่คือเหตุผลที่ชัดเจนว่าทำไมงานกิน CPU หนัก (image processing, parse หนัก ๆ) เป็นเหตุผลที่ดีที่จะเลือก Edge Function ตั้งแต่แรก และทำไม function ที่พยายามทำ in-memory aggregation หนัก ๆ จาก row ที่ดึงมาจาก Postgres ถึงชนกับ limit นี้ตรง ๆ — ดู bug ด้านล่าง
เมื่อไหร่ที่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ที่ไม่ควรใช้”อีกด้านของ “Edge Functions ใช้กับ logic ที่ไม่ใช่ database query” คือกับดักที่พบบ่อย: ดึง row ออกจาก Postgres เข้ามาใน function แค่เพื่อทำงานที่ Postgres เองทำได้เร็วกว่า ไม่มี network round trip และไม่มี CPU-time budget ต้องสู้ ถ้างานคือ “aggregate/filter/join row” มันควรอยู่ใน Postgres function ที่เรียกผ่าน /rpc — ดู Database Functions as RPC — ไม่ใช่อยู่ใน Deno isolate ที่มี CPU budget แค่ 2 วินาที
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”AI ถูกขอให้เขียน “endpoint ที่คืนยอดขายรวมของ order ทั้งหมด” แล้วได้โค้ดนี้มา
🐛 หา bug ในโค้ดนี้
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (_req) => {const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,)
// Pull every order row into the function and sum it in JavaScriptconst { data: orders } = await supabase.from('orders').select('amount')const total = (orders ?? []).reduce((sum, o) => sum + o.amount, 0)
return new Response(JSON.stringify({ total }), { headers: { 'Content-Type': 'application/json' },})})เฉลย
โค้ดนี้ทำงานได้บน table เล็ก ๆ แล้วค่อย ๆ แย่ลงเรื่อย ๆ เมื่อ orders โตขึ้น: ทุก row ถูกดึงผ่าน network เข้ามาใน isolate เก็บไว้ใน memory แล้ว sum ด้วย loop JavaScript อยู่ใน runtime ที่มี memory ceiling 256MB และ CPU-time budget 2 วินาทีต่อ request table ที่มี row เป็นแสนจะชน memory limit หรือกิน CPU-time budget ไปกับการ reduce หรือทั้งสองอย่าง และวิธีแก้ไม่เกี่ยวกับ Edge Functions เลย: select sum(amount) from orders คำนวณเลขเดียวกันในตัว Postgres รอบเดียว ด้วย engine ที่สร้างมาเพื่อ aggregate row อย่างมีประสิทธิภาพ เวอร์ชันที่แก้แล้วเรียก supabase.rpc('total_revenue') (หรือแม้แต่ aggregate สไตล์ .select('amount').limit(0) ผ่าน PostgREST) ที่ backing ด้วย Postgres function แบบ security invoker, stable — pattern “database functions as RPC” จากโมดูลก่อนหน้าเลย ส่วน Edge Function ถ้ายังจำเป็นอยู่ ก็ให้เหลือไว้ทำแค่สิ่งที่ RPC call ทำไม่ได้ เช่น ตรวจสิทธิ์, เรียก third-party, หรือจัดรูปแบบ response ให้ client เฉพาะราย
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การเขียน Edge Function คือการเขียนโปรแกรม Deno เดี่ยว ๆ ที่มี module graph ของตัวเอง ไม่ใช่การเพิ่ม route ให้แอป Node.js ที่มีอยู่แล้ว ทุก import ถูก resolve ใหม่ต่อ function ตอน isolate boot ขึ้นมา นี่คือเหตุผลที่ bare specifier พังตอน boot (ก่อนที่ handler code ของคุณจะรันเลย) ไม่ใช่แค่พังที่บรรทัดที่ใช้มัน และทำไม deno.json ที่อยู่ข้าง index.ts ของ function หนึ่งจึงไม่มีผลกับ function อื่นเลย CORS และ verify_jwt อยู่ที่ boundary “ก่อนโค้ดคุณรัน” เดียวกันนี้ verify_jwt ถูกตรวจโดย runtime ก่อน handler ของคุณถูกเรียกด้วยซ้ำ (พิสูจน์ด้วย response body ที่มี structure ชัดเจน ไม่ใช่สิ่งที่โค้ดคุณสร้างเอง) ส่วน CORS header กลับกัน ไม่มีอะไรตรวจให้คุณเลย นี่คือเหตุผลที่ function ที่ไม่ตั้ง CORS header เลย “ทำงานได้” ทุกที่ที่ทดสอบด้วย curl หรือ browser same-origin แต่จะพังแบบเงียบ ๆ ทันทีที่ browser client จากคนละ origin จริง ๆ เรียกมา
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- Edge Functions รัน TypeScript อะไรก็ได้ เข้าถึงอินเทอร์เน็ตได้เต็มที่ — แต่ต้องแลกกับ memory ceiling ตายตัว (256MB) และ CPU-time budget (2s/request) ที่ Postgres function ไม่ต้องคิดถึงเลย เพราะ Postgres เองเป็นตัวที่คำนวณตรงนั้น
- isolate ที่ warm ตอบ request เร็วกว่า cold ประมาณ 10 เท่า (วัดข้างบน) — แต่ต้องแลกกับความไม่แน่นอน: request หนึ่งจะไปเจอ isolate warm หรือ cold ขึ้นกับ traffic ล่าสุด ไม่ใช่อะไรที่โค้ดของ function เองควบคุมได้
supabase functions serveในเครื่องรัน Edge Runtime binary ตัวเดียวกันเป๊ะกับบนโฮสต์จริง — แต่ต้องแลกกับการไม่มีการกระจายตามภูมิศาสตร์และ secrets store ของโฮสต์จริงในเครื่อง:.env/--env-fileทำหน้าที่แทนsupabase secrets setและมี instance เดียว ไม่ใช่หนึ่งต่อ region
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Edge Function ก็แค่ server Node.js เล็ก ๆ” มันรันบน Deno อยู่ใน runtime ที่ sandbox ออกแบบมาเฉพาะ มี permission model และ resource limit ของตัวเอง โค้ดที่คาดว่าจะมี Node.js API หรือ import แบบ bare specifier สไตล์ npm จะไม่ทำงานถ้าไม่แก้ก่อน ตามที่บทถัดไปพูดถึง
- “ถ้า function ช้า แค่เพิ่ม memory/time ให้มันก็พอ” limit บนโฮสต์จริง (256MB, CPU time 2s, wall clock 150s/400s) เป็น ceiling ตายตัวของแพลตฟอร์ม ไม่ใช่ปุ่มที่หมุนได้ function ที่ต้องการมากกว่านั้นมักทำงานที่ควรอยู่ที่อื่น (Postgres สำหรับ aggregation, queue/background job สำหรับงานที่ยาวจริง ๆ)
- “Edge Functions รันอยู่ที่เดียวกับฐานข้อมูลเสมอ” มัน deploy ไปยัง global edge network และรันใกล้ request ไม่ใช่ใกล้ฐานข้อมูล function ที่ทำ round trip ไป Postgres หลายรอบต่อเนื่องกันจะเจอ network latency จริงต่อรอบ ไม่เหมือน SQL ของ Postgres function ที่รันติดกับข้อมูลของตัวเองเลย
- “CPU-time limit แปลว่า function รันได้แค่ 2 วินาที” budget 2 วินาทีคือ CPU time ที่ใช้ คำนวณ จริง ไม่ใช่ wall-clock time function รันได้นานกว่า 2 วินาทีจริงได้ ถ้าเวลาส่วนใหญ่หมดไปกับการ
awaitI/O ทาง network (third-party API ที่ช้า, database query) ไม่ใช่การกิน CPU