Membangun Chatbot Streaming dengan Groq dan Next.js

Foto oleh Ioan Sameli on flickr
Groq menjalankan model open source di atas perangkat keras LPU khusus, bukan GPU serba guna, sehingga menghasilkan throughput token keluaran yang sangat tinggi dan waktu ke token pertama yang rendah. Untuk UI chat streaming, hal ini langsung berarti pengalaman yang terasa lebih cepat, karena kecepatan yang dirasakan bergantung pada seberapa cepat token terus muncul setelah token pertama, bukan total waktu permintaan.
Set stream menjadi true saat memanggil groq.chat.completions.create, lalu iterasi generator async yang dikembalikan dengan loop for-await di dalam callback start milik ReadableStream. Konten delta setiap chunk di-encode sebagai server-sent event dan dimasukkan ke controller, dan Response dibuat dengan stream tersebut serta content type text/event-stream.
Bisa. response.body pada Fetch API menyediakan ReadableStream yang bisa dibaca langsung dengan getReader, mendekode setiap chunk dengan TextDecoder lalu memisahkannya berdasarkan baris kosong ganda yang memisahkan setiap server-sent event. Tidak perlu EventSource atau dependency tambahan, yang penting di sini karena EventSource hanya mendukung permintaan GET, bukan POST.
Nyatakan tujuan tunggal dan cakupan topik asisten pada kalimat pertama, tambahkan instruksi eksplisit untuk mengabaikan permintaan tersisip dalam input pengguna yang mencoba mengganti peran asisten, dan ulangi batasan cakupan itu menjelang akhir prompt agar tetap bertahan pada percakapan panjang. Padukan ini dengan validasi sisi server yang membatasi panjang pesan dan menolak frasa prompt injection yang dikenal sebelum permintaan sampai ke model.
Terapkan rate limit per pengunjung milikmu sendiri, berdasarkan alamat IP, diset jauh di bawah batas paket Groq untuk permintaan dan token per menit, serta batasi max_completion_tokens pada setiap permintaan agar satu balasan tidak bisa menghabiskan token tanpa batas. Groq juga mengembalikan data usage per respons, sehingga mencatatnya membantu menangkap lonjakan biaya akibat bug atau klien yang menyalahgunakan sebelum tagihan bulanan yang menyadarkanmu.

Foto oleh Ioan Sameli on flickr
Ringkasan Utama
Groq menjalankan model di atas chip LPU sehingga token pertama muncul dalam hitungan milidetik; dipadukan dengan Server-Sent Events dan route handler Next.js berbasis ReadableStream, chatbot bisa menampilkan balasan sedikit demi sedikit alih-alih menunggu respons utuh. Agar siap produksi, API key harus disimpan di server, terapkan rate limiting per pengunjung, batasi max_completion_tokens, dan perkuat system prompt terhadap prompt injection.
Sebagian besar demo chatbot terasa lambat karena menunggu seluruh respons selesai sebelum menampilkan apa pun. Chatbot produksi seharusnya terasa instan, dan dua bahan yang membuat itu mungkin adalah penyedia inferensi yang benar-benar cepat serta pipeline streaming yang mengirim token ke browser begitu token itu tersedia, bukan menyimpan seluruh balasan di server terlebih dahulu.
Tulisan ini membahas pembuatan pipeline tersebut secara menyeluruh: route handler Next.js App Router yang memanggil Groq API dengan mode streaming aktif, klien yang membaca respons secara bertahap, system prompt yang menjaga asisten tetap pada topiknya, serta rate limiting dan kontrol biaya yang dibutuhkan sebelum ini digunakan untuk trafik sungguhan.
Groq menjalankan model open source seperti Llama dan GPT OSS di atas perangkat keras LPU (Language Processing Unit) khusus, bukan GPU serba guna, sehingga chat completions API-nya mampu mempertahankan throughput token keluaran yang sangat tinggi. Untuk chatbot streaming, throughput ini lebih penting daripada sekadar kualitas model, karena kecepatan yang dirasakan pengguna pada antarmuka chat lebih ditentukan oleh seberapa cepat token berikutnya muncul setelah token pertama, bukan oleh total waktu permintaan.
Pilih model berdasarkan throughput dan context window yang dibutuhkan use case-mu, bukan hanya skor benchmark. Model yang lebih kecil dan cepat yang streaming secara instan sering memberi pengalaman yang terasa lebih baik dibanding model besar yang sedikit lebih akurat tapi terlihat lambat saat mulai merespons.
Di App Router, route handler hanyalah fungsi yang menerima Web Request dan mengembalikan Web Response, artinya kamu bisa mengembalikan ReadableStream mentah alih-alih body JSON. Itulah persis yang dibutuhkan chat streaming: handler membuka koneksi ke Groq dengan stream diset true, lalu meneruskan setiap token sebagai server-sent event begitu token itu tiba.
Handler di bawah ini menyimpan sumber kebenaran percakapan di server, menyusun array messages dengan system prompt ditambah pesan pengguna yang masuk, lalu meneruskan setiap delta dari stream Groq ke klien menggunakan format standar Server-Sent Events, yaitu baris berawalan data titik dua diikuti payload JSON dan baris kosong. Membungkus seluruh pemanggilan dalam blok try dan finally penting di sini: jika permintaan ke Groq gagal di tengah jalan, blok finally tetap menutup controller sehingga koneksi klien tidak menggantung tanpa batas waktu.
// app/api/chat/route.ts
import { NextRequest } from "next/server"
import Groq from "groq-sdk"
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
export async function POST(request: NextRequest) {
const { message } = await request.json()
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
const completion = await groq.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: message },
],
max_completion_tokens: 1024,
temperature: 0.7,
stream: true,
})
for await (const chunk of completion) {
const delta = chunk.choices[0]?.delta?.content
if (delta) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ delta })}\n\n`)
)
}
}
controller.enqueue(encoder.encode('data: {"done":true}\n\n'))
} catch (err) {
controller.enqueue(
encoder.encode('data: {"error":"stream_failed"}\n\n')
)
} finally {
controller.close()
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
})
}Di sisi klien, kamu tidak memerlukan objek EventSource karena permintaannya adalah POST dengan body, bukan GET biasa. Sebagai gantinya, baca body respons langsung menggunakan reader streaming dari Fetch API, decode setiap potongan byte menjadi teks, lalu pisahkan berdasarkan baris kosong ganda yang memisahkan setiap server-sent event.
Simpan sementara (buffer) setiap event yang datang terpotong di antara dua chunk, karena TCP tidak menjamin satu event utuh selalu tiba dalam satu kali pembacaan. Setelah kamu melihat flag done pada payload, hentikan pembacaan dan finalisasi pesan di state UI-mu. Ada baiknya juga merender teks yang terkumpul setiap kali delta tiba alih-alih menunggu seluruh pesan selesai, karena repaint bertahap itulah inti visual dari streaming itu sendiri; klien yang menyimpan semuanya dulu sebelum memperbarui layar membuang manfaat latensi yang seharusnya diberikan throughput Groq.
// client component
async function sendMessage(message: string, onDelta: (text: string) => void) {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.startsWith("data: ")) continue
const payload = JSON.parse(line.slice(6))
if (payload.delta) onDelta(payload.delta)
if (payload.done) return
}
}
}Jangan memanggil Groq API langsung dari browser dengan API key tertanam di kode klien. Setiap permintaan harus melewati route handler milikmu sendiri agar key tetap berada di server; key yang bocor di halaman chatbot publik bisa diambil dan dipakai untuk menjalankan trafik orang lain dengan biaya ditanggung akunmu dalam hitungan menit.
Chatbot yang menjawab apa saja adalah risiko dukungan dan penyalahgunaan. System prompt harus menyatakan tujuan tunggal asisten secara jelas, mendaftarkan hal yang harus ditolaknya, dan menyertakan pengingat singkat menjelang akhir prompt untuk menegaskan kembali batasan tersebut, karena percakapan panjang bisa melemahkan instruksi di awal. Menyisipkan string canary unik sebagai komentar HTML di dalam system prompt juga membantu; jika string itu muncul kembali terbawa dalam pesan pengguna, itu sinyal kuat bahwa seseorang berhasil mengekstrak prompt-mu dan sedang mengujinya kembali ke endpoint milikmu sendiri.
Groq menerapkan batas per API key di beberapa dimensi: permintaan per menit, token per menit, dan batas harian untuk keduanya. Melampaui batas mana pun akan mengembalikan status 429 disertai header retry-after yang memberitahu berapa detik harus menunggu. Aplikasimu sendiri sebaiknya menerapkan batas yang lebih ketat per pengunjung jauh sebelum menyentuh batas Groq, baik untuk mengendalikan biaya maupun mencegah satu klien yang menyalahgunakan sistem membuat pengguna lain kelaparan sumber daya.
| Tingkatan | Batas permintaan umum | Batas token umum |
|---|---|---|
| Tingkat gratis | 30 permintaan per menit | 6.000 token per menit untuk sebagian besar model |
| Tingkat developer | Hingga 1.000 permintaan per menit | Hingga kurang lebih 250.000 sampai 300.000 token per menit |
| Melebihi batas mana pun | Respons 429 Too Many Requests | Header retry-after menyatakan waktu tunggu dalam detik |
Sebelum memanggil Groq, tambahkan rate limiter per pengunjung berdasarkan alamat IP, dibatasi jauh di bawah batas paketmu. Batasi juga max_completion_tokens pada setiap permintaan; panjang completion yang tidak dibatasi adalah cara termudah membuat model yang murah per token menjadi tagihan mahal ketika pengguna (atau scraper) mengirim prompt yang sama berulang-ulang.
Chat streaming terlihat sederhana dalam demo tetapi bisa gagal dengan cara yang spesifik dan bisa diprediksi di produksi. Periksa daftar ini sebelum mengarahkan trafik sungguhan ke route handler, dan tinjau ulang setelah rilis begitu pola pemakaian nyata mulai terlihat di log-mu.