Load Testing Laravel dengan k6: Simulasi 1000 Users Sebelum Launching

Pelajari cara melakukan load testing pada aplikasi Laravel menggunakan k6 untuk simulasi 1000+ concurrent users sebelum launch. Artikel ini membahas studi kasus nyata dari konsultasi dengan seorang freelancer yang membangun AI-powered flight booking system untuk startup client.

Dari panik H-3 launch karena website crash di 50 users, sampai akhirnya bisa handle 1800 users dengan lancar. Lengkap dengan script k6 yang bisa langsung dipakai, cara interpretasi hasil testing, identifikasi bottleneck, dan strategi fix yang proven. Jangan sampai launch day jadi disaster day.


Bagian 1: The Consultation — Tiga Hari Sebelum Launch

Tiga hari sebelum launch, saya dapat DM di LinkedIn dari seorang freelancer yang terdengar panik.

"Kak Angga, boleh konsultasi sebentar? Urgent banget. Saya butuh second opinion."

Namanya Fajar — bukan nama sebenarnya, saya ganti untuk menjaga privasi. Dia freelancer Laravel dengan pengalaman sekitar 3 tahun. Dari chat-nya, saya bisa rasakan anxiety-nya. Ini project terbesar yang pernah dia handle.

"Saya lagi build aplikasi flight booking untuk startup travel, Kak. Mereka funded, jadi expect-nya tinggi. Ada AI recommendation juga untuk suggest penerbangan terbaik berdasarkan preferensi user."

Kedengarannya impressive. AI-powered flight booking bukan project sederhana.

"Website udah jadi, tinggal launch hari Jumat. Tapi saya nervous, Kak. Client bilang mereka mau ads besar-besaran di launch day. Expect 1000+ users langsung di hari pertama. Saya... gimana ya, feeling nggak enak aja."

Saya langsung tanya satu pertanyaan yang menurut saya krusial:

"Sudah load test belum?"

Ada jeda beberapa detik sebelum dia reply.

"Load test... itu yang gimana, Kak? Maksudnya test di browser banyak tab gitu?"

Dan di situlah alarm di kepala saya berbunyi.

The Project Context

Sebelum lanjut, saya minta Fajar jelaskan lebih detail tentang projectnya:

AspectDetails
ClientStartup travel (funded, marketing budget besar)
ProductAI-powered flight booking platform
Development Time3 bulan
Tech StackLaravel 12, Vue.js, MySQL, Redis (belum terpakai)
ServerVPS 4 core, 8GB RAM
Launch PlanJumat ini, dengan paid ads campaign
Expected Traffic1000+ users di hari pertama

Fitur-fitur yang sudah dibangun:

  • Flight search dengan integrasi ke multiple airlines API
  • AI recommendation engine (suggest penerbangan terbaik)
  • Real-time pricing dari berbagai maskapai
  • Booking flow dengan payment gateway integration
  • User accounts, booking history, saved preferences

Secara fitur, aplikasinya sudah lengkap. Fajar sudah invest 3 bulan penuh. Client sudah siapkan budget marketing puluhan juta untuk launch day campaign.

Tapi tidak satu pun dari mereka yang tau jawaban pertanyaan paling fundamental:

Website ini sebenarnya bisa handle berapa users?

The False Confidence

"Di local sih lancar, Kak. Saya test sendiri pakai beberapa browser tabs, buka-buka halaman, oke-oke aja. Nggak ada error."

Saya harus jujur ke Fajar.

"Itu bukan load test. Itu cuma verifikasi bahwa website bisa jalan. Beda jauh."

"Maksudnya gimana, Kak?"

"Kamu test dengan 1 user — diri kamu sendiri. Mungkin 3-5 browser tabs. Production akan ada ratusan atau ribuan request bersamaan. Itu universe yang completely different."

"Tapi kan server saya lumayan, 4 core 8GB RAM..."

"Specs server bagus tidak guarantee performance bagus. Yang determine performance itu gimana code kamu handle concurrent load. Database queries, API calls, memory management — semua itu bisa jadi bottleneck."

Fajar diam sebentar.

"Jadi... gimana cara tau website saya bisa handle berapa users?"

"Kita load test. Simulasi ratusan atau ribuan users mengakses website kamu bersamaan, dan lihat gimana sistem respond."

"Testing dengan 3 browser tabs dan berharap website siap untuk 1000 users itu seperti latihan jogging santai di taman dan expect bisa finish marathon. Sama-sama lari, tapi completely different game. Yang satu rekreasi, yang satu survival."


Bagian 2: Apa Itu Load Testing dan Kenapa Kamu Harus Peduli

Sebelum kita setup tools, saya jelaskan dulu ke Fajar — dan sekarang ke kamu — apa sebenarnya load testing itu.

Definisi Sederhana

Load testing adalah proses simulasi dimana kamu mengirim ratusan atau ribuan "fake users" ke aplikasi kamu secara bersamaan, lalu mengamati bagaimana sistem merespons.

Bayangkan seperti stress test untuk jantung di rumah sakit. Dokter tidak cuma tanya "gimana perasaan Anda?" — dokter minta kamu lari di treadmill sambil monitor detak jantung, tekanan darah, dan oxygen level. Dokter ingin tau gimana jantung kamu perform under pressure, bukan saat santai.

Load testing sama. Kita ingin tau gimana server, database, dan aplikasi kamu perform ketika ratusan orang akses bersamaan — bukan saat kamu sendiri yang test.

Analogi: Grand Opening Restaurant

Untuk bikin lebih konkret, bayangkan kamu mau buka restaurant baru.

SCENARIO A: Tanpa "Load Test"

Hari H Grand Opening:
├── "Menu udah enak, pasti rame!"
├── Buka pintu, 200 orang langsung masuk
├── Dapur overwhelmed — cuma siap untuk 30 orders/jam
├── Pesanan delay 1-2 jam
├── Customer marah, komplain di social media
├── Review bintang 1 bertebaran
└── Reputation rusak di hari pertama

SCENARIO B: Dengan "Load Test"

2 Minggu Sebelum Opening:
├── Simulasi: Minta 200 teman datang dan order bersamaan
├── Discover: Dapur cuma bisa handle 30 orders/jam
├── Problem identified! Fix sebelum opening:
│   ├── Tambah 2 chef
│   ├── Optimize workflow dapur
│   ├── Pre-prep ingredients
│   └── Streamline menu
├── Re-test: Sekarang bisa 250 orders/jam
├── Grand opening: Smooth, customer happy
└── Review bagus, reputation solid dari hari pertama

Load testing adalah "simulasi grand opening" untuk website kamu. Lebih baik tau masalah sekarang daripada ketika real customers sudah di depan pintu.

Jenis-Jenis Load Test

Tidak semua load test sama. Ada beberapa jenis dengan tujuan berbeda:

TypePurposeLoad PatternWhen to Use
Smoke TestVerify sistem bisa jalan dengan minimal load1-5 users, 1-2 menitSanity check sebelum test lain
Average Load TestTest performa di traffic normalRamp up ke expected daily loadRegular performance check
Stress TestCari breaking point sistemIncrease sampai sistem gagalTau limit maksimal
Spike TestTest sudden traffic surgeJump drastis dalam waktu singkatFlash sale, viral moment
Soak TestTest endurance jangka panjangSteady load selama berjam-jamCari memory leaks
Breakpoint TestCari exact maximum capacityGradual increase sampai crashCapacity planning

Visual pattern masing-masing test:

SMOKE TEST           AVERAGE LOAD         STRESS TEST
Users                Users                Users
│                    │      ___           │           /
│___                 │    /    \\          │         /
│                    │  /        \\        │       /
└────── Time         └─────────── Time    └─────── Time
(flat, minimal)      (ramp up, hold,      (keep increasing
                      ramp down)           until break)

SPIKE TEST           SOAK TEST            BREAKPOINT
Users                Users                Users
│    __              │ _______________    │                /
│   /  \\             │                    │              /
│  /    \\            │                    │            /
│ /      \\           │                    │          /
└────────── Time     └─────────────Time   └───────── Time
(sudden jump,        (steady for hours)   (find exact limit)
 then drop)

Untuk case Fajar dengan launch day yang mendekat, kita butuh minimal:

  1. Smoke test — pastikan basic functionality works
  2. Average load test — simulasi expected traffic (1000 users)
  3. Stress test — cari breaking point, tau limit

Kenapa k6?

Ada banyak load testing tools di luar sana. Kenapa saya recommend k6?

ToolLanguageLearning CurveCI/CD ReadyFreeNotes
k6JavaScriptRendah✅ Excellent✅ YesModern, developer-friendly
JMeterJava/XMLTinggi⚠️ Complex✅ YesPowerful tapi berat
GatlingScalaMedium✅ Good✅ YesBagus untuk Scala devs
LocustPythonRendah✅ Good✅ YesBagus untuk Python devs
ArtilleryJavaScriptRendah✅ Good✅ YesSimple, YAML config
LoadRunnerProprietaryTinggi✅ Enterprise❌ PaidEnterprise solution

Kenapa k6 jadi pilihan saya:

  1. JavaScript Syntax — Familiar untuk web developers. Tidak perlu belajar bahasa baru.
  2. Lightweight — Bisa generate ribuan virtual users dari satu laptop. Tidak butuh cluster untuk test basic.
  3. Built-in Metrics — Langsung dapat metrics yang meaningful: response time, throughput, error rate, percentiles.
  4. Thresholds — Bisa define "pass/fail" criteria. Kalau response time > 2 detik, test otomatis fail.
  5. CI/CD Friendly — Easy integration dengan GitHub Actions, GitLab CI, Jenkins. Automate testing.
  6. Open Source — Free, active community, backed by Grafana Labs.
  7. Scriptable — Full programming power. Bisa simulate complex user journeys, bukan cuma hit single endpoint.

"Oke Kak, saya convinced. Gimana cara mulai?"

"Load testing bukan tentang 'apakah website saya cepat'. Load testing tentang 'apakah website saya TETAP cepat ketika 1000 orang akses bersamaan'. Dua pertanyaan yang sangat berbeda, dan banyak developer baru sadar bedanya ketika production sudah terbakar."


Next: Bagian 3 — Setup k6 dan First Test

Di bagian selanjutnya, kita akan install k6, tulis test script pertama, dan mulai testing aplikasi Fajar. Spoiler: hasilnya akan mengejutkan.


Bagian 3: Setup k6 dan Test Pertama

Sekarang saatnya hands-on. Saya guide Fajar untuk install k6 dan tulis test script pertamanya.

Instalasi k6

k6 tersedia untuk semua major operating systems. Pilih sesuai yang kamu pakai:

macOS (menggunakan Homebrew):

brew install k6

Ubuntu/Debian:

sudo gpg -k
sudo gpg --no-default-keyring \\
    --keyring /usr/share/keyrings/k6-archive-keyring.gpg \\
    --keyserver hkp://keyserver.ubuntu.com:80 \\
    --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69

echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] <https://dl.k6.io/deb> stable main" \\
    | sudo tee /etc/apt/sources.list.d/k6.list

sudo apt-get update
sudo apt-get install k6

Windows (menggunakan Chocolatey):

choco install k6

Atau download binary langsung:

# Download dari GitHub releases
<https://github.com/grafana/k6/releases>

# Pilih versi sesuai OS kamu
# Extract dan tambahkan ke PATH

Verifikasi instalasi:

k6 version

# Output:
# k6 v0.50.0 (go1.21.6, linux/amd64)

Script Pertama: Smoke Test

Smoke test adalah test paling basic — cuma untuk verify bahwa sistem bisa diakses dan respond dengan benar. Seperti cek apakah mobil bisa nyala sebelum test drive jauh.

Buat file tests/smoke-test.js:

// tests/smoke-test.js
// Smoke Test: Verify basic functionality works

import http from 'k6/http';
import { check, sleep } from 'k6';

// Test configuration
export const options = {
    vus: 1,              // 1 virtual user (just you)
    duration: '30s',     // Run for 30 seconds
    thresholds: {
        http_req_duration: ['p(95)<2000'],  // 95% requests under 2s
        http_req_failed: ['rate<0.01'],      // Less than 1% failed
    },
};

// The test scenario
export default function () {
    // Hit the homepage
    const response = http.get('<https://your-app.com/>');

    // Verify response
    check(response, {
        'status is 200': (r) => r.status === 200,
        'response time OK': (r) => r.timings.duration < 2000,
        'has content': (r) => r.body.length > 0,
    });

    // Wait before next request (simulates user "thinking")
    sleep(1);
}

Penjelasan kode:

// IMPORTS
import http from 'k6/http';     // Untuk HTTP requests
import { check, sleep } from 'k6';  // Untuk assertions dan delay

// OPTIONS — Konfigurasi test
export const options = {
    vus: 1,           // Virtual Users — berapa "fake users" yang jalan
    duration: '30s',  // Berapa lama test berjalan
    thresholds: {     // Pass/fail criteria
        http_req_duration: ['p(95)<2000'],  // 95th percentile < 2 detik
        http_req_failed: ['rate<0.01'],     // Error rate < 1%
    },
};

// DEFAULT FUNCTION — Yang dijalankan setiap VU
export default function () {
    // Ini akan di-loop selama durasi test
    // Setiap VU akan jalankan function ini berulang-ulang
}

Menjalankan Test

# Run smoke test
k6 run tests/smoke-test.js

# Output akan muncul seperti ini:

          /\\      |‾‾| /‾‾/   /‾‾/
     /\\  /  \\     |  |/  /   /  /
    /  \\/    \\    |     (   /   ‾‾\\
   /          \\   |  |\\  \\ |  (‾)  |
  / __________ \\  |__| \\__\\ \\_____/ .io

  execution: local
     script: tests/smoke-test.js
     output: -

  scenarios: (100.00%) 1 scenario, 1 max VUs, 1m0s max duration
           default: 1 looping VUs for 30s

running (0m30.5s), 0/1 VUs, 28 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  30s

     ✓ status is 200
     ✓ response time OK
     ✓ has content

     checks.........................: 100.00% ✓ 84       ✗ 0
     data_received..................: 156 kB  5.1 kB/s
     data_sent......................: 3.2 kB  105 B/s
     http_req_blocked...............: avg=1.2ms   min=1µs    max=35ms   p(90)=2µs    p(95)=2µs
     http_req_connecting............: avg=0.8ms   min=0µs    max=24ms   p(90)=0µs    p(95)=0µs
     http_req_duration..............: avg=187ms   min=142ms  max=312ms  p(90)=245ms  p(95)=278ms
     http_req_failed................: 0.00%   ✓ 0        ✗ 28
     http_req_receiving.............: avg=1.5ms   min=0.8ms  max=4.2ms  p(90)=2.1ms  p(95)=2.8ms
     http_req_sending...............: avg=0.1ms   min=0.05ms max=0.3ms  p(90)=0.2ms  p(95)=0.2ms
     http_req_waiting...............: avg=185ms   min=140ms  max=308ms  p(90)=242ms  p(95)=275ms
     http_reqs......................: 28      0.917699/s
     iteration_duration.............: avg=1.19s   min=1.14s  max=1.32s  p(90)=1.25s  p(95)=1.28s
     iterations.....................: 28      0.917699/s
     vus............................: 1       min=1      max=1
     vus_max........................: 1       min=1      max=1

     ✓ http_req_duration: p(95)<2000
     ✓ http_req_failed: rate<0.01

Membaca Hasil Test

Hasil k6 bisa overwhelming kalau pertama kali lihat. Ini breakdown metrics yang paling penting:

MetricArtiNilai Bagus
http_req_durationTotal waktu request (termasuk download)< 500ms untuk web, < 100ms untuk API
http_req_waitingTime to First Byte (TTFB) — waktu tunggu server< 200ms
http_req_failedPersentase request yang gagal< 1%
http_reqsTotal requests yang dibuatHigher = better throughput
checksPass rate dari assertions kamu100% ideal
p(90)90th percentile — 90% requests lebih cepat dari iniIndikator realistic
p(95)95th percentile — 95% requests lebih cepat dari iniBiasa dipakai untuk SLA
vusVirtual Users yang aktifSesuai konfigurasi

Analogi untuk Percentiles:

Bayangkan 100 requests, diurutkan dari tercepat ke terlambat:

Request #1:   45ms   ← Tercepat
Request #2:   52ms
...
Request #90:  245ms  ← p(90) — 90% requests lebih cepat dari ini
...
Request #95:  278ms  ← p(95) — 95% requests lebih cepat dari ini
...
Request #100: 312ms  ← Terlambat (max)

Average: 187ms (tapi bisa misleading kalau ada outliers)
p(95): 278ms (lebih representative untuk "worst case yang normal")

Pro tip: Jangan terlalu fokus ke "average". Percentiles (p90, p95, p99) lebih meaningful karena menunjukkan experience dari majority users, bukan di-skew oleh outliers.


Bagian 4: Testing Aplikasi Flight Booking Fajar

Smoke test passed — basic functionality works. Sekarang saatnya test yang sebenarnya: simulasi real user behavior dengan multiple concurrent users.

Menyusun Test Scenario

Untuk aplikasi flight booking, user journey typical-nya seperti ini:

TYPICAL USER JOURNEY:

1. 🏠 Buka Homepage
   └── Lihat featured destinations, promo

2. 🔍 Search Flights
   └── Input origin, destination, dates, passengers
   └── Submit search

3. 🤖 Get AI Recommendation (optional)
   └── Minta AI suggest best flight

4. 📋 View Flight Details
   └── Lihat detail, bagasi, meals, dll

5. 🛒 Start Booking
   └── Isi passenger details, contact info

6. 💳 Payment
   └── Pilih metode, proses bayar

Untuk load test, kita simulasi step 1-5.
Step 6 (payment) biasanya di-mock untuk avoid real transactions.

Script Load Test untuk Flight Booking

// tests/flight-booking-load-test.js
// Load Test: Simulate real user journey on flight booking app

import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';

// ============ CUSTOM METRICS ============
// Track specific endpoints separately
const errorRate = new Rate('errors');
const homepageDuration = new Trend('homepage_duration');
const searchDuration = new Trend('search_duration');
const aiRecommendationDuration = new Trend('ai_recommendation_duration');
const bookingDuration = new Trend('booking_duration');

// ============ TEST CONFIGURATION ============
export const options = {
    // Ramp up pattern: gradually increase load
    stages: [
        { duration: '30s', target: 10 },    // Warm up: 0 → 10 users
        { duration: '1m', target: 10 },     // Hold at 10 users
        { duration: '30s', target: 50 },    // Ramp: 10 → 50 users
        { duration: '1m', target: 50 },     // Hold at 50 users
        { duration: '30s', target: 100 },   // Ramp: 50 → 100 users
        { duration: '1m', target: 100 },    // Hold at 100 users
        { duration: '30s', target: 0 },     // Ramp down: 100 → 0
    ],

    // Pass/fail thresholds
    thresholds: {
        http_req_duration: ['p(95)<2000'],           // Overall: 95% < 2s
        http_req_failed: ['rate<0.1'],                // Error rate < 10%
        errors: ['rate<0.1'],                         // Custom error rate < 10%
        homepage_duration: ['p(95)<1000'],            // Homepage < 1s
        search_duration: ['p(95)<3000'],              // Search < 3s
        ai_recommendation_duration: ['p(95)<5000'],   // AI rec < 5s
        booking_duration: ['p(95)<2000'],             // Booking page < 2s
    },
};

// Base URL — configurable via environment variable
const BASE_URL = __ENV.BASE_URL || '<https://staging.flightbooking.com>';

// ============ TEST DATA ============
const routes = [
    { origin: 'CGK', destination: 'DPS', name: 'Jakarta-Bali' },
    { origin: 'CGK', destination: 'SUB', name: 'Jakarta-Surabaya' },
    { origin: 'CGK', destination: 'JOG', name: 'Jakarta-Jogja' },
    { origin: 'SUB', destination: 'DPS', name: 'Surabaya-Bali' },
    { origin: 'JOG', destination: 'DPS', name: 'Jogja-Bali' },
];

const passengerCounts = [1, 2, 3, 4];

// ============ HELPER FUNCTIONS ============
function randomBetween(min, max) {
    return Math.random() * (max - min) + min;
}

function randomItem(array) {
    return array[Math.floor(Math.random() * array.length)];
}

function getRandomDate(daysFromNow) {
    const date = new Date();
    date.setDate(date.getDate() + daysFromNow);
    return date.toISOString().split('T')[0];
}

// ============ MAIN TEST SCENARIO ============
export default function () {
    const route = randomItem(routes);
    const passengers = randomItem(passengerCounts);
    const departureDate = getRandomDate(Math.floor(randomBetween(7, 60)));

    // -------- 1. HOMEPAGE --------
    group('01_Homepage', function () {
        const res = http.get(`${BASE_URL}/`, {
            tags: { name: 'homepage' },
        });

        homepageDuration.add(res.timings.duration);

        const success = check(res, {
            'homepage: status 200': (r) => r.status === 200,
            'homepage: has content': (r) => r.body.includes('flight') || r.body.includes('Flight'),
            'homepage: < 1s': (r) => r.timings.duration < 1000,
        });

        errorRate.add(!success);
    });

    sleep(randomBetween(1, 3)); // User browses homepage

    // -------- 2. SEARCH FLIGHTS --------
    group('02_Flight_Search', function () {
        const searchPayload = JSON.stringify({
            origin: route.origin,
            destination: route.destination,
            departure_date: departureDate,
            passengers: passengers,
            class: 'economy',
        });

        const res = http.post(`${BASE_URL}/api/flights/search`, searchPayload, {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            tags: { name: 'flight_search' },
        });

        searchDuration.add(res.timings.duration);

        const success = check(res, {
            'search: status 200': (r) => r.status === 200,
            'search: has results': (r) => {
                try {
                    const body = JSON.parse(r.body);
                    return body.data && Array.isArray(body.data);
                } catch (e) {
                    return false;
                }
            },
            'search: < 3s': (r) => r.timings.duration < 3000,
        });

        errorRate.add(!success);
    });

    sleep(randomBetween(2, 5)); // User reviews search results

    // -------- 3. AI RECOMMENDATION --------
    group('03_AI_Recommendation', function () {
        const aiPayload = JSON.stringify({
            origin: route.origin,
            destination: route.destination,
            departure_date: departureDate,
            preferences: {
                budget: randomItem(['low', 'medium', 'high']),
                priority: randomItem(['price', 'duration', 'departure_time']),
            },
        });

        const res = http.post(`${BASE_URL}/api/ai/recommend`, aiPayload, {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            tags: { name: 'ai_recommendation' },
        });

        aiRecommendationDuration.add(res.timings.duration);

        const success = check(res, {
            'AI: status 200': (r) => r.status === 200,
            'AI: has recommendation': (r) => {
                try {
                    const body = JSON.parse(r.body);
                    return body.recommendation !== undefined;
                } catch (e) {
                    return false;
                }
            },
            'AI: < 5s': (r) => r.timings.duration < 5000,
        });

        errorRate.add(!success);
    });

    sleep(randomBetween(1, 3)); // User considers recommendation

    // -------- 4. VIEW FLIGHT DETAILS --------
    group('04_Flight_Details', function () {
        // Simulate clicking on a specific flight
        const flightId = Math.floor(randomBetween(1, 100));

        const res = http.get(`${BASE_URL}/api/flights/${flightId}`, {
            headers: { 'Accept': 'application/json' },
            tags: { name: 'flight_details' },
        });

        const success = check(res, {
            'details: status 200 or 404': (r) => r.status === 200 || r.status === 404,
            'details: < 2s': (r) => r.timings.duration < 2000,
        });

        errorRate.add(res.status >= 500); // Only count server errors
    });

    sleep(randomBetween(2, 4)); // User reads details

    // -------- 5. START BOOKING --------
    group('05_Booking_Page', function () {
        const flightId = Math.floor(randomBetween(1, 100));

        const res = http.get(`${BASE_URL}/booking/new?flight_id=${flightId}`, {
            tags: { name: 'booking_page' },
        });

        bookingDuration.add(res.timings.duration);

        const success = check(res, {
            'booking: status 200': (r) => r.status === 200,
            'booking: has form': (r) => r.body.includes('form') || r.body.includes('passenger'),
            'booking: < 2s': (r) => r.timings.duration < 2000,
        });

        errorRate.add(!success);
    });

    sleep(randomBetween(1, 2)); // Small delay before next iteration
}

// ============ SETUP & TEARDOWN ============
export function setup() {
    console.log(`Starting load test against: ${BASE_URL}`);
    console.log(`Test will ramp up to 100 concurrent users`);

    // Verify target is reachable
    const res = http.get(`${BASE_URL}/`);
    if (res.status !== 200) {
        throw new Error(`Target not reachable: ${BASE_URL} returned ${res.status}`);
    }

    return { startTime: new Date().toISOString() };
}

export function teardown(data) {
    console.log(`Test started at: ${data.startTime}`);
    console.log(`Test ended at: ${new Date().toISOString()}`);
}

Menjalankan Test

# Run dengan environment variable untuk target URL
k6 run -e BASE_URL=https://staging.flightbooking.com tests/flight-booking-load-test.js

# Save hasil ke file JSON untuk analysis lebih lanjut
k6 run -e BASE_URL=https://staging.flightbooking.com \\
    --out json=results.json \\
    tests/flight-booking-load-test.js

# Dengan summary export
k6 run -e BASE_URL=https://staging.flightbooking.com \\
    --summary-export=summary.json \\
    tests/flight-booking-load-test.js

Hasil Test: Kenyataan yang Mengejutkan

Fajar menjalankan test. Kami berdua watch hasilnya real-time.

Di 10 users, masih oke. Sedikit lambat tapi acceptable.

Di 30 users, mulai ada warning signs. Response time naik.

Di 50 users... disaster.

❌ HASIL TEST DI 50 CONCURRENT USERS:

running (5m30.2s), 000/100 VUs, 3847 complete and 47 interrupted iterations
default ✗ [======================================] 100 VUs  5m30s

     ✗ homepage: status 200
      ↳  89% — ✓ 3423 / ✗ 424
     ✗ homepage: < 1s
      ↳  52% — ✓ 2000 / ✗ 1847
     ✗ search: status 200
      ↳  72% — ✓ 2772 / ✗ 1075
     ✗ search: < 3s
      ↳  45% — ✓ 1731 / ✗ 2116
     ✗ AI: status 200
      ↳  61% — ✓ 2347 / ✗ 1500
     ✗ AI: < 5s
      ↳  38% — ✓ 1462 / ✗ 2385
     ✗ booking: status 200
      ↳  78% — ✓ 3001 / ✗ 846

     checks.........................: 62.33%  ✓ 16736    ✗ 10193
     errors..........................: 28.43%  ✓ 7667     ✗ 19313

   ✗ http_req_duration..............: avg=4.23s   min=89ms   max=32.4s  p(90)=8.52s  p(95)=12.31s
       { name:ai_recommendation }...: avg=8.34s   min=1.2s   max=45.2s  p(90)=18.2s  p(95)=25.41s
       { name:booking_page }........: avg=2.12s   min=234ms  max=12.3s  p(90)=4.1s   p(95)=5.8s
       { name:flight_search }.......: avg=5.87s   min=890ms  max=28.7s  p(90)=9.23s  p(95)=15.12s
       { name:homepage }............: avg=1.24s   min=145ms  max=8.9s   p(90)=2.8s   p(95)=4.2s
     http_req_failed................: 22.31%  ✓ 8567     ✗ 29814

     iterations.....................: 3847    11.65/s
     vus............................: 47      min=1      max=100
     vus_max........................: 100     min=100    max=100

THRESHOLDS CROSSED:
   ✗ http_req_duration: p(95)<2000
      ↳ actual: 12310ms — FAILED ❌
   ✗ http_req_failed: rate<0.1
      ↳ actual: 22.31% — FAILED ❌
   ✗ errors: rate<0.1
      ↳ actual: 28.43% — FAILED ❌
   ✗ search_duration: p(95)<3000
      ↳ actual: 15120ms — FAILED ❌
   ✗ ai_recommendation_duration: p(95)<5000
      ↳ actual: 25410ms — FAILED ❌

Interpretasi Hasil

Saya bantu Fajar membaca hasilnya:

MetricExpectedActualStatus
Error rate< 10%28.43%FAILED — Hampir 1 dari 3 requests gagal
Overall p(95)< 2s12.31sFAILED — 6x lebih lambat dari target
Search p(95)< 3s15.12sFAILED — 5x lebih lambat
AI Recommendation p(95)< 5s25.41sFAILED — 5x lebih lambat
Homepage p(95)< 1s4.2sFAILED — 4x lebih lambat
Booking p(95)< 2s5.8sFAILED — 3x lebih lambat

Yang paling mengkhawatirkan:

  1. AI Recommendation — Average 8.34 detik, max 45 detik. Ada request yang butuh hampir 1 menit!
  2. Flight Search — Average hampir 6 detik. Ini endpoint yang paling sering diakses.
  3. Error Rate 28% — Hampir sepertiga requests gagal total. User akan lihat error page.
  4. Test bahkan belum sampai 100 users — Website sudah mulai collapse di 50 users. Target 1000 users? Impossible.

Reality Check untuk Fajar

"Kak... ini artinya apa?"

"Artinya, website kamu tidak siap untuk 50 users sekalipun, apalagi 1000."

"Tapi di local..."

"Di local kamu satu-satunya user. Tidak ada competition untuk CPU, memory, database connections. Production completely different."

Saya tunjukkan analogi:

ANALOGI: JALAN TOL

Local Development (1 user):
════════════════════════════════════════════
    🚗
════════════════════════════════════════════
Jalan kosong, kamu satu-satunya mobil.
Jakarta-Bandung 2 jam. Easy.

Production (50+ users):
════════════════════════════════════════════
🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗🚗
════════════════════════════════════════════
Macet total. Semua rebutan jalan yang sama.
Jakarta-Bandung 8 jam. Nightmare.

Dan kamu expect 1000 mobil (users)?
Jalan tolnya perlu di-expand atau ada yang salah.

"Jadi... launch Jumat nggak bisa?"

"Kalau launch dengan kondisi sekarang, hari pertama adalah bencana. Error everywhere, users frustrated, bad reviews, client marah. Career damage."

"Terus gimana, Kak?"

"Kita identify bottleneck-nya dimana, fix, dan test ulang. Masih ada 3 hari. Tight, tapi bisa kalau fokus."

"Lebih baik delay launch 1 minggu daripada launch disaster yang akan haunting reputation kamu selamanya. Client mungkin kecewa dengan delay. Tapi client akan LEBIH kecewa kalau launch day adalah hari dimana website mereka tidak bisa diakses."


Next: Bagian 5 — Identifying Bottlenecks

Di bagian selanjutnya, kita akan deep-dive ke code Fajar dan identify exactly dimana bottleneck-nya. Spoiler: ada beberapa "classic mistakes" yang sering terjadi.


Bagian 5: Identifying Bottlenecks — Dimana Masalahnya?

Dari hasil k6 test, kita sudah tau website Fajar tidak perform. Tapi k6 hanya menunjukkan symptoms. Untuk fix, kita perlu tau root cause.

Breakdown dari Metrics

Mari analisis hasil test lebih detail:

EndpointAvg Responsep(95)Error RateSeverity
AI Recommendation8.34s25.4s39%🔴 Critical
Flight Search5.87s15.1s28%🔴 Critical
Booking Page2.12s5.8s22%🟠 High
Homepage1.24s4.2s11%🟡 Medium

Prioritas fix: AI Recommendation → Flight Search → Booking → Homepage

"Kak, gimana cara tau masalahnya di code yang mana?"

"Kita perlu lihat code kamu. Share screen."

Problem #1: AI Recommendation — Synchronous Blocking Call

Ini code AI recommendation yang Fajar tulis:

// ❌ BEFORE: app/Services/AIRecommendationService.php

namespace App\\Services;

use Illuminate\\Support\\Facades\\Http;

class AIRecommendationService
{
    public function getRecommendation(array $params)
    {
        // PROBLEM: Blocking HTTP call dengan timeout 30 detik
        // Setiap request NUNGGU sampai AI service respond
        // Kalau 50 requests masuk bersamaan, semua nunggu

        $response = Http::timeout(30)->post(
            '<https://ai-service.example.com/recommend>',
            [
                'flights' => $this->getAllAvailableFlights($params),
                'user_preferences' => $params['preferences'],
                'historical_data' => $this->getUserHistory($params['user_id'] ?? null),
            ]
        );

        if ($response->failed()) {
            throw new \\Exception('AI service unavailable');
        }

        return $response->json();
    }

    private function getAllAvailableFlights(array $params)
    {
        // PROBLEM: Load SEMUA flights ke memory dulu
        // Baru kirim ke AI service
        return Flight::where('origin', $params['origin'])
            ->where('destination', $params['destination'])
            ->where('departure_date', $params['departure_date'])
            ->with(['airline', 'prices', 'reviews']) // Heavy relations
            ->get()
            ->toArray();
    }
}

Masalah yang saya identify:

ANALOGI: RESTAURANT DENGAN 1 CHEF

Request masuk:
├── Request 1: "Minta AI recommendation" → Chef mulai masak (30 detik)
├── Request 2: "Minta AI recommendation" → TUNGGU, chef masih masak
├── Request 3: "Minta AI recommendation" → TUNGGU di belakang Request 2
├── ...
└── Request 50: TUNGGU 25+ menit untuk giliran

Ini yang terjadi:
• HTTP call blocking — PHP worker TIDAK BISA handle request lain
• Timeout 30 detik — Terlalu lama, resources locked
• getAllAvailableFlights() load semua data dulu — Memory spike
• Tidak ada caching — Query yang sama diulang terus

Problem #2: Flight Search — N+1 Query Hell

// ❌ BEFORE: app/Services/FlightSearchService.php

namespace App\\Services;

class FlightSearchService
{
    protected $airlines = [
        'garuda', 'lionair', 'airasia', 'citilink', 'batikair'
    ];

    public function search(array $params)
    {
        $results = [];

        // PROBLEM 1: Sequential API calls
        // 5 airlines × 1-2 detik each = 5-10 detik total
        foreach ($this->airlines as $airline) {
            $results[] = $this->fetchFromAirline($airline, $params);
        }

        // PROBLEM 2: Query database untuk additional info
        $flights = Flight::where('route', $params['origin'] . '-' . $params['destination'])
            ->where('departure_date', $params['departure_date'])
            ->get();

        // PROBLEM 3: N+1 Query — setiap flight trigger query baru
        foreach ($flights as $flight) {
            // Query 1 per flight
            $flight->current_price = $flight->prices()->latest()->first();

            // Query 2 per flight
            $flight->airline_data = $flight->airline;

            // Query 3 per flight
            $flight->average_rating = $flight->reviews()->avg('rating');

            // Query 4 per flight
            $flight->total_reviews = $flight->reviews()->count();
        }

        // Kalau ada 100 flights:
        // 1 (initial) + 100×4 (N+1) = 401 queries!

        return $flights;
    }

    private function fetchFromAirline($airline, $params)
    {
        // Each call: 1-2 seconds
        return Http::timeout(10)
            ->get("<https://api>.{$airline}.com/flights", $params)
            ->json();
    }
}

Visual N+1 Problem:

N+1 QUERY PROBLEM VISUALIZED:

Untuk 100 flights:

Query #1:   SELECT * FROM flights WHERE route = ? AND date = ?
            └── Returns 100 flights

Query #2:   SELECT * FROM prices WHERE flight_id = 1 ORDER BY created_at DESC LIMIT 1
Query #3:   SELECT * FROM prices WHERE flight_id = 2 ORDER BY created_at DESC LIMIT 1
Query #4:   SELECT * FROM prices WHERE flight_id = 3 ORDER BY created_at DESC LIMIT 1
...
Query #101: SELECT * FROM prices WHERE flight_id = 100 ORDER BY created_at DESC LIMIT 1

Query #102: SELECT * FROM airlines WHERE id = 5
Query #103: SELECT * FROM airlines WHERE id = 3
...
Query #201: SELECT * FROM airlines WHERE id = 7

Query #202: SELECT AVG(rating) FROM reviews WHERE flight_id = 1
...
Query #301: SELECT AVG(rating) FROM reviews WHERE flight_id = 100

Query #302: SELECT COUNT(*) FROM reviews WHERE flight_id = 1
...
Query #401: SELECT COUNT(*) FROM reviews WHERE flight_id = 100

TOTAL: 401 QUERIES untuk 1 search request!
Kalau 50 users search bersamaan: 20,050 queries!
Database: 💀

Problem #3: No Caching

// ❌ BEFORE: Tidak ada caching sama sekali

// Homepage — query setiap request
public function index()
{
    // Query ini jalan SETIAP kali homepage diakses
    $featuredDestinations = Destination::withCount('flights')
        ->orderBy('flights_count', 'desc')
        ->limit(10)
        ->get();

    // Query ini juga jalan setiap request
    $popularFlights = Flight::with(['origin', 'destination'])
        ->orderBy('bookings_count', 'desc')
        ->limit(8)
        ->get();

    // Padahal data ini jarang berubah!
    // Tapi di-query 1000x kalau ada 1000 visitors
}

Problem #4: Database Connection Exhaustion

Saya minta Fajar check MySQL saat test jalan:

-- Check active connections
SHOW STATUS LIKE 'Threads_connected';
-- Result: 152 (max_connections = 150) — EXCEEDED!

-- Check running queries
SHOW STATUS LIKE 'Threads_running';
-- Result: 148 — Almost all connections busy

-- Check waiting connections
SHOW STATUS LIKE 'Threads_cached';
-- Result: 0 — No cached threads available

-- See what's happening
SHOW FULL PROCESSLIST;
-- Result: Banyak queries dalam state "Waiting for table lock"

Diagnosis:

DATABASE CONNECTION POOL EXHAUSTED:

┌─────────────────────────────────────────────────────────────┐
│                     MySQL Server                            │
│                  max_connections = 150                      │
├─────────────────────────────────────────────────────────────┤
│ [Conn 1] Flight search query... RUNNING                    │
│ [Conn 2] AI recommendation query... RUNNING                │
│ [Conn 3] Flight search query... RUNNING                    │
│ ...                                                         │
│ [Conn 150] Homepage query... RUNNING                       │
├─────────────────────────────────────────────────────────────┤
│ [QUEUE] New request → WAITING (no connection available)    │
│ [QUEUE] New request → WAITING                              │
│ [QUEUE] New request → WAITING                              │
│ [QUEUE] New request → TIMEOUT after 30s → ERROR 500        │
└─────────────────────────────────────────────────────────────┘

Setiap request HOLD connection sampai selesai.
Kalau query lambat (5-10 detik), connection di-hold 5-10 detik.
50 slow requests = 50 connections held = pool hampir habis.
Request ke-51+ harus TUNGGU atau TIMEOUT.

Summary of Issues

┌────────────────────────────────────────────────────────────────┐
│                    BOTTLENECK SUMMARY                          │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  1. AI RECOMMENDATION (avg 8.3s)                               │
│     ├── Synchronous blocking HTTP call                        │
│     ├── 30 second timeout (terlalu lama)                      │
│     ├── Load semua flights ke memory sebelum kirim            │
│     └── Tidak ada queue/async processing                      │
│                                                                │
│  2. FLIGHT SEARCH (avg 5.8s)                                   │
│     ├── Sequential airline API calls (bukan parallel)         │
│     ├── N+1 queries (401 queries per search!)                 │
│     ├── Tidak ada eager loading                               │
│     └── Tidak ada caching                                     │
│                                                                │
│  3. DATABASE                                                   │
│     ├── Connection pool exhausted (150/150)                   │
│     ├── Queries tidak optimized                               │
│     ├── Tidak ada indexing review                             │
│     └── Tidak ada query result caching                        │
│                                                                │
│  4. GENERAL                                                    │
│     ├── Redis ada tapi tidak dipakai                          │
│     ├── Session disimpan di database                          │
│     ├── Tidak ada CDN untuk assets                            │
│     └── Tidak ada rate limiting                               │
│                                                                │
└────────────────────────────────────────────────────────────────┘

"Kak... ini banyak banget yang salah."

"Iya. Tapi kabar baiknya, semua ini fixable. Dan pattern-nya common — banyak developer bikin mistake yang sama. Let's fix one by one."


Bagian 6: The Fix — Step by Step

Kita punya 3 hari. Tight, tapi cukup kalau fokus. Saya prioritaskan fixes berdasarkan impact.

Fix #1: Parallel API Calls (Impact: High)

Ubah dari sequential ke parallel menggunakan Laravel HTTP Pool:

// ✅ AFTER: app/Services/FlightSearchService.php

namespace App\\Services;

use Illuminate\\Support\\Facades\\Http;
use Illuminate\\Http\\Client\\Pool;

class FlightSearchService
{
    protected array $airlines = [
        'garuda' => '<https://api.garuda.com/flights>',
        'lionair' => '<https://api.lionair.com/flights>',
        'airasia' => '<https://api.airasia.com/flights>',
        'citilink' => '<https://api.citilink.com/flights>',
        'batikair' => '<https://api.batikair.com/flights>',
    ];

    public function searchFromAPIs(array $params): array
    {
        // PARALLEL — Semua API dipanggil BERSAMAAN
        $responses = Http::pool(fn (Pool $pool) =>
            collect($this->airlines)->map(fn ($url, $code) =>
                $pool->as($code)
                    ->timeout(5)      // Timeout lebih pendek
                    ->retry(2, 100)   // Retry 2x dengan delay 100ms
                    ->get($url, [
                        'origin' => $params['origin'],
                        'destination' => $params['destination'],
                        'date' => $params['departure_date'],
                        'passengers' => $params['passengers'],
                    ])
            )->toArray()
        );

        // Combine successful responses
        return collect($responses)
            ->filter(fn ($response) => $response->successful())
            ->flatMap(fn ($response) => $response->json('flights') ?? [])
            ->toArray();
    }
}

// BEFORE: 5 airlines × 1.5s each (sequential) = 7.5 seconds
// AFTER:  5 airlines parallel = 1.5 seconds (slowest one)
// IMPROVEMENT: 5x faster

Analogi:

SEQUENTIAL (Before):
┌─────────────────────────────────────────────────────┐
│ Garuda ████████ (1.5s)                              │
│                 Citilink ████████ (1.5s)            │
│                                   Lion █████ (1s)   │
│                                        AirAsia ████ │
│                                              Batik █│
└─────────────────────────────────────────────────────┘
Total: 7+ seconds (sum of all)

PARALLEL (After):
┌─────────────────────────────────────────────────────┐
│ Garuda   ████████                                   │
│ Citilink ████████                                   │
│ Lion     █████                                      │
│ AirAsia  ████                                       │
│ Batik    ███                                        │
└─────────────────────────────────────────────────────┘
Total: 1.5 seconds (slowest one)

Fix #2: Eager Loading — Kill N+1 (Impact: Critical)

// ✅ AFTER: app/Services/FlightSearchService.php

public function searchFromDatabase(array $params): Collection
{
    return Flight::query()
        // Select hanya columns yang dibutuhkan
        ->select([
            'id', 'flight_number', 'origin', 'destination',
            'departure_time', 'arrival_time', 'airline_id',
            'base_price', 'available_seats'
        ])
        // EAGER LOAD — Load relations dalam 1 query
        ->with([
            'airline:id,name,code,logo',
            'latestPrice',  // Sudah di-define di model sebagai hasOne
        ])
        // SUBQUERY untuk aggregates — tidak perlu load semua reviews
        ->withAvg('reviews', 'rating')
        ->withCount('reviews')
        // Filters
        ->where('origin', $params['origin'])
        ->where('destination', $params['destination'])
        ->where('departure_date', $params['departure_date'])
        ->where('available_seats', '>=', $params['passengers'] ?? 1)
        ->where('status', 'active')
        // Sorting
        ->orderBy('departure_time')
        ->get();
}

// Di Model Flight, tambahkan relationship:
// app/Models/Flight.php

public function latestPrice(): HasOne
{
    return $this->hasOne(FlightPrice::class)->latestOfMany();
}

// BEFORE: 401 queries untuk 100 flights
// AFTER: 3 queries total (flights + airlines + prices)
// IMPROVEMENT: 133x fewer queries!

Fix #3: Redis Caching (Impact: High)

// ✅ AFTER: app/Services/FlightSearchService.php

use Illuminate\\Support\\Facades\\Cache;

public function search(array $params): Collection
{
    // Build cache key dari search params
    $cacheKey = $this->buildCacheKey($params);

    // Cache selama 5 menit (harga bisa berubah, jadi TTL pendek)
    return Cache::remember($cacheKey, now()->addMinutes(5), function () use ($params) {
        // Combine API results + database results
        $apiResults = $this->searchFromAPIs($params);
        $dbResults = $this->searchFromDatabase($params);

        return $this->mergeAndSort($apiResults, $dbResults);
    });
}

private function buildCacheKey(array $params): string
{
    // Sort params untuk consistency
    ksort($params);

    // Generate unique key
    $hash = md5(json_encode([
        'origin' => $params['origin'],
        'destination' => $params['destination'],
        'date' => $params['departure_date'],
        'passengers' => $params['passengers'] ?? 1,
    ]));

    return "flights:search:{$hash}";
}

// Request pertama: Execute search, cache result
// Request ke-2 sampai ke-1000 (dalam 5 menit): Return dari cache (< 5ms)

Jangan lupa update .env:

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
REDIS_CACHE_DB=1
REDIS_SESSION_DB=2
REDIS_QUEUE_DB=3

Fix #4: Queue untuk AI Recommendation (Impact: Critical)

Ini fix yang paling significant — ubah dari synchronous ke asynchronous:

// ✅ AFTER: app/Http/Controllers/AIRecommendationController.php

namespace App\\Http\\Controllers;

use App\\Jobs\\ProcessAIRecommendation;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Str;
use Illuminate\\Support\\Facades\\Cache;

class AIRecommendationController extends Controller
{
    /**
     * Request AI recommendation — returns immediately
     */
    public function request(Request $request)
    {
        $validated = $request->validate([
            'origin' => 'required|string|size:3',
            'destination' => 'required|string|size:3',
            'departure_date' => 'required|date|after:today',
            'preferences' => 'required|array',
        ]);

        // Generate unique request ID
        $requestId = Str::uuid()->toString();

        // Mark as processing
        Cache::put("ai:request:{$requestId}:status", 'processing', now()->addMinutes(10));

        // Dispatch ke queue — TIDAK BLOCKING
        ProcessAIRecommendation::dispatch($requestId, $validated);

        // Return immediately dengan request ID
        return response()->json([
            'request_id' => $requestId,
            'status' => 'processing',
            'message' => 'AI recommendation sedang diproses',
            'poll_url' => route('ai.recommendation.status', $requestId),
            'estimated_time' => '5-10 seconds',
        ], 202); // 202 Accepted
    }

    /**
     * Check status — frontend polls this
     */
    public function status(string $requestId)
    {
        $status = Cache::get("ai:request:{$requestId}:status");

        if (!$status) {
            return response()->json([
                'status' => 'not_found',
                'message' => 'Request tidak ditemukan atau sudah expired',
            ], 404);
        }

        if ($status === 'processing') {
            return response()->json([
                'status' => 'processing',
                'message' => 'Masih diproses, coba lagi dalam beberapa detik',
            ]);
        }

        if ($status === 'completed') {
            $result = Cache::get("ai:request:{$requestId}:result");

            return response()->json([
                'status' => 'completed',
                'recommendation' => $result,
            ]);
        }

        if ($status === 'failed') {
            $error = Cache::get("ai:request:{$requestId}:error");

            return response()->json([
                'status' => 'failed',
                'message' => $error ?? 'Terjadi kesalahan',
            ], 500);
        }
    }
}
// ✅ AFTER: app/Jobs/ProcessAIRecommendation.php

namespace App\\Jobs;

use Illuminate\\Bus\\Queueable;
use Illuminate\\Contracts\\Queue\\ShouldQueue;
use Illuminate\\Foundation\\Bus\\Dispatchable;
use Illuminate\\Queue\\InteractsWithQueue;
use Illuminate\\Queue\\SerializesModels;
use Illuminate\\Support\\Facades\\Cache;
use Illuminate\\Support\\Facades\\Http;
use Illuminate\\Support\\Facades\\Log;

class ProcessAIRecommendation implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;           // Retry 3x if failed
    public int $timeout = 60;        // Max 60 seconds per attempt
    public int $backoff = 5;         // Wait 5s between retries

    public function __construct(
        public string $requestId,
        public array $params
    ) {}

    public function handle(): void
    {
        try {
            // Call AI service
            $response = Http::timeout(30)
                ->retry(2, 1000)
                ->post('<https://ai-service.example.com/recommend>', [
                    'origin' => $this->params['origin'],
                    'destination' => $this->params['destination'],
                    'date' => $this->params['departure_date'],
                    'preferences' => $this->params['preferences'],
                ]);

            if ($response->successful()) {
                // Store result
                Cache::put(
                    "ai:request:{$this->requestId}:result",
                    $response->json(),
                    now()->addMinutes(30)
                );
                Cache::put(
                    "ai:request:{$this->requestId}:status",
                    'completed',
                    now()->addMinutes(30)
                );
            } else {
                throw new \\Exception('AI service returned error: ' . $response->status());
            }

        } catch (\\Exception $e) {
            Log::error('AI Recommendation failed', [
                'request_id' => $this->requestId,
                'error' => $e->getMessage(),
            ]);

            Cache::put(
                "ai:request:{$this->requestId}:error",
                'Gagal mendapatkan rekomendasi. Silakan coba lagi.',
                now()->addMinutes(10)
            );
            Cache::put(
                "ai:request:{$this->requestId}:status",
                'failed',
                now()->addMinutes(10)
            );
        }
    }
}

Frontend perlu diupdate untuk polling:

// Frontend: Poll untuk result
async function getAIRecommendation(params) {
    // 1. Request recommendation
    const response = await fetch('/api/ai/recommend', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
    });

    const { request_id, poll_url } = await response.json();

    // 2. Poll for result
    return pollForResult(poll_url, 30); // Max 30 seconds
}

async function pollForResult(url, maxSeconds) {
    const startTime = Date.now();

    while ((Date.now() - startTime) / 1000 < maxSeconds) {
        const response = await fetch(url);
        const data = await response.json();

        if (data.status === 'completed') {
            return data.recommendation;
        }

        if (data.status === 'failed') {
            throw new Error(data.message);
        }

        // Wait 1 second before next poll
        await new Promise(resolve => setTimeout(resolve, 1000));
    }

    throw new Error('Request timeout');
}

Visual perbandingan:

BEFORE (Synchronous):
┌──────────────────────────────────────────────────────────────┐
│ Request → [PHP Worker LOCKED untuk 30 detik] → Response     │
│           ↑ Worker tidak bisa handle request lain           │
└──────────────────────────────────────────────────────────────┘

AFTER (Async dengan Queue):
┌──────────────────────────────────────────────────────────────┐
│ Request → Return immediately (50ms) → Worker FREE           │
│               ↓                                              │
│        Queue Job → [Background Worker processes] → Cache    │
│               ↓                                              │
│        Frontend polls → Get result from Cache               │
└──────────────────────────────────────────────────────────────┘

Fix #5: Database Optimization

// config/database.php — Optimize connection settings

'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),

    // Connection pooling settings
    'options' => [
        PDO::ATTR_PERSISTENT => true,  // Persistent connections
        PDO::ATTR_EMULATE_PREPARES => true,
    ],

    // Timeout settings
    'timeout' => 10,
],
-- Tambah indexes yang diperlukan

-- Index untuk flight search
CREATE INDEX idx_flights_search ON flights(origin, destination, departure_date, status);

-- Index untuk reviews aggregate
CREATE INDEX idx_reviews_flight_rating ON reviews(flight_id, rating);

-- Index untuk prices
CREATE INDEX idx_prices_flight_latest ON flight_prices(flight_id, created_at DESC);

Summary of All Fixes

FixBeforeAfterImprovement
Airline API callsSequential (7.5s)Parallel (1.5s)5x faster
Database queries401 per search3 per search133x fewer
Search cachingNone5-min Redis cache80%+ cache hit
AI RecommendationSync blocking (30s)Async queue (50ms response)Non-blocking
Session storageDatabaseRedis10x faster
DB connectionsExhausted (150/150)Stable (30-50)No exhaustion

Bagian 7: Re-Testing — Moment of Truth

Setelah implement semua fixes, saatnya test ulang. Ini moment of truth.

Running the Same Test

k6 run -e BASE_URL=https://staging.flightbooking.com tests/flight-booking-load-test.js

The New Results

✅ HASIL TEST SETELAH OPTIMIZATION (100 Concurrent Users):

running (5m30.1s), 000/100 VUs, 12847 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs  5m30s

     ✓ homepage: status 200
      ↳  99.9% — ✓ 12834 / ✗ 13
     ✓ homepage: < 1s
      ↳  99.2% — ✓ 12744 / ✗ 103
     ✓ search: status 200
      ↳  99.7% — ✓ 12808 / ✗ 39
     ✓ search: < 3s
      ↳  98.4% — ✓ 12641 / ✗ 206
     ✓ AI: status 200
      ↳  99.5% — ✓ 12782 / ✗ 65
     ✓ AI: < 5s
      ↳  99.8% — ✓ 12821 / ✗ 26
     ✓ booking: status 200
      ↳  99.8% — ✓ 12821 / ✗ 26

     checks.........................: 99.47%  ✓ 89451    ✗ 478
     errors..........................: 0.53%   ✓ 478      ✗ 89451

   ✓ http_req_duration..............: avg=287ms   min=23ms   max=2.1s   p(90)=456ms  p(95)=612ms
       { name:ai_recommendation }...: avg=156ms   min=45ms   max=890ms  p(90)=298ms  p(95)=412ms
       { name:booking_page }........: avg=234ms   min=34ms   max=1.2s   p(90)=389ms  p(95)=523ms
       { name:flight_search }.......: avg=412ms   min=89ms   max=1.8s   p(90)=678ms  p(95)=892ms
       { name:homepage }............: avg=178ms   min=23ms   max=756ms  p(90)=312ms  p(95)=423ms
     http_req_failed................: 0.38%   ✓ 489      ✗ 128234

     iterations.....................: 12847   38.92/s
     vus............................: 100     min=1      max=100
     vus_max........................: 100     min=100    max=100

THRESHOLDS:
   ✓ http_req_duration: p(95)<2000 — PASSED (actual: 612ms) ✅
   ✓ http_req_failed: rate<0.1 — PASSED (actual: 0.38%) ✅
   ✓ errors: rate<0.1 — PASSED (actual: 0.53%) ✅
   ✓ search_duration: p(95)<3000 — PASSED (actual: 892ms) ✅
   ✓ ai_recommendation_duration: p(95)<5000 — PASSED (actual: 412ms) ✅

Before vs After Comparison

╔═══════════════════════════════════════════════════════════════════════╗
║                    PERFORMANCE COMPARISON                             ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                       ║
║   METRIC                    BEFORE           AFTER          CHANGE   ║
║   ─────────────────────────────────────────────────────────────────   ║
║   Error Rate                28.43%           0.53%          ↓ 98%    ║
║   Overall Response p(95)    12.31s           612ms          ↓ 95%    ║
║   Search p(95)              15.12s           892ms          ↓ 94%    ║
║   AI Recommendation p(95)   25.41s           412ms          ↓ 98%    ║
║   Homepage p(95)            4.2s             423ms          ↓ 90%    ║
║   Booking p(95)             5.8s             523ms          ↓ 91%    ║
║   Throughput                11.65 req/s      38.92 req/s    ↑ 234%   ║
║                                                                       ║
╚═══════════════════════════════════════════════════════════════════════╝

Stress Test — Finding the Real Limit

Sekarang 100 users passed. Tapi target 1000 users. Let's push harder:

// tests/stress-test.js

export const options = {
    stages: [
        { duration: '2m', target: 200 },
        { duration: '3m', target: 200 },
        { duration: '2m', target: 500 },
        { duration: '3m', target: 500 },
        { duration: '2m', target: 1000 },
        { duration: '3m', target: 1000 },
        { duration: '2m', target: 1500 },
        { duration: '3m', target: 1500 },
        { duration: '2m', target: 0 },
    ],
    thresholds: {
        http_req_duration: ['p(95)<3000'],
        http_req_failed: ['rate<0.05'],
    },
};

Hasil Stress Test:

STRESS TEST RESULTS:

200 users:  ✅ PASSED — p(95) = 523ms, error rate = 0.3%
500 users:  ✅ PASSED — p(95) = 845ms, error rate = 0.8%
1000 users: ✅ PASSED — p(95) = 1.4s, error rate = 1.2%
1500 users: ✅ PASSED — p(95) = 2.3s, error rate = 2.1%
1800 users: ⚠️ WARNING — p(95) = 3.8s, error rate = 4.8%
2000 users: ❌ DEGRADED — p(95) = 5.2s, error rate = 8.3%

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   BREAKING POINT: ~1800 concurrent users                   │
│   SAFE CAPACITY: ~1500 concurrent users (with headroom)    │
│                                                             │
│   Target was 1000 users                                     │
│   We can handle 1500 users = 50% HEADROOM ✅                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Fajar's Reaction

"Kak... dari 50 users crash, sekarang 1500 users aman?"

"Yep. Dan lebih penting — sekarang kamu tau limitnya. Kalau nanti traffic exceed 1500, kamu tau harus scale. Add more workers, bigger server, atau implement horizontal scaling. Tidak kaget di production."

"Ini... honestly saya nggak expect. Cuma 3 hari tapi beda drastis."

"Itulah power of proper optimization. Banyak apps sebenarnya bisa handle high traffic — cuma perlu identify dan fix bottlenecks. Most performance issues bukan karena infrastructure kurang, tapi karena code tidak optimal."

Launch Day

Jumat pagi. Launch day.

Client sudah siap dengan ads campaign. Marketing team standby. Semua mata tertuju ke dashboard analytics.

Traffic mulai naik. 100 users... 300... 500... 800...

Peak di jam 8 malam: 1,247 concurrent users.

Website tetap responsive. Response time rata-rata 600ms. Error rate 0.4%.

Zero downtime. Zero panic. Zero angry customers.

"Kak, makasih banyak. Seriously. Kalau nggak ketemu Kak Angga, entah gimana nasib saya dan project ini."

"Yang penting kamu belajar. Next project, load test dari awal. Jangan tunggu H-3."

"Tiga hari dari disaster ke success. Bukan karena magic — tapi karena systematic: identify bottlenecks, fix one by one, test, verify. Load testing adalah safety net yang menyelamatkan banyak launches dari menjadi disasters."


Next: Bagian 8-10 — Best Practices, CI/CD Integration, dan Closing

Di bagian terakhir, kita akan cover best practices untuk load testing, cara integrate ke CI/CD pipeline, dan rekomendasi untuk learning lebih lanjut.


Bagian 8: Best Practices dan Lessons Learned

Dari pengalaman dengan Fajar dan puluhan project lainnya, ini best practices yang saya kumpulkan untuk load testing.

Kapan Harus Load Test

Jangan tunggu H-3 launch seperti Fajar. Ini timeline ideal:

MilestoneJenis TestTujuan
Development (setiap fitur besar)Smoke TestVerify basic functionality
Sebelum merge ke stagingAverage LoadCheck tidak ada regression
Sebelum deploy ke productionStress TestFind breaking point
Setelah major releaseFull SuiteEnsure nothing broke
Regular (mingguan/bulanan)Soak TestDetect memory leaks
Sebelum expected traffic spikeSpike TestVerify dapat handle surge

Setting Thresholds yang Realistic

Jangan asal set threshold. Base on real requirements:

// ✅ GOOD: Thresholds based on actual SLA/requirements
export const options = {
    thresholds: {
        // Response time targets (based on UX research)
        // Users expect pages to load in under 3 seconds
        http_req_duration: ['p(95)<2000'],    // 95% under 2s
        http_req_duration: ['p(99)<5000'],    // 99% under 5s (allow some outliers)

        // Error rate (based on business requirements)
        http_req_failed: ['rate<0.01'],       // Less than 1% errors

        // Per-endpoint thresholds (different expectations)
        'http_req_duration{name:api}': ['p(95)<500'],     // API should be fast
        'http_req_duration{name:search}': ['p(95)<3000'], // Search can be slower
        'http_req_duration{name:static}': ['p(95)<100'],  // Static files must be instant

        // Custom business metrics
        'checkout_success_rate': ['rate>0.95'],  // 95% checkouts succeed
    },
};

// ❌ BAD: Arbitrary thresholds without reasoning
export const options = {
    thresholds: {
        http_req_duration: ['p(95)<100'],  // Unrealistic for most apps
    },
};

Tips untuk Test yang Akurat

ACCURATE LOAD TESTING CHECKLIST:

1. TEST ENVIRONMENT
   □ Test staging, BUKAN localhost
   □ Staging harus mirror production (specs, data, config)
   □ Network conditions realistic
   □ Database size production-like

2. TEST DATA
   □ Gunakan data realistic (bukan 10 records)
   □ Variety dalam test data (different routes, users, etc.)
   □ Randomize untuk simulate real behavior

3. THINK TIME
   □ Tambahkan delay antar requests (users tidak instant)
   □ Randomize delays (1-5 detik typical)
   □ Model real user behavior

4. WARM UP
   □ Run warm-up sebelum actual test
   □ Biarkan cache populate
   □ Connection pools stabilize

5. MONITORING
   □ Watch server metrics selama test
   □ Monitor database (connections, queries)
   □ Track external services (APIs, Redis)

6. MULTIPLE RUNS
   □ Jangan rely pada 1x run
   □ Run minimal 3x untuk consistency
   □ Check for variance

Common Mistakes yang Harus Dihindari

// ❌ MISTAKE 1: Testing localhost
k6 run -e BASE_URL=http://localhost:8000 test.js
// Network latency = 0, tidak realistic

// ✅ FIX: Test against staging/production-like environment
k6 run -e BASE_URL=https://staging.yourapp.com test.js

// ❌ MISTAKE 2: No think time — robot behavior
export default function () {
    http.get('/page1');
    http.get('/page2');  // Instant, no delay
    http.get('/page3');  // Tidak realistik
}

// ✅ FIX: Add realistic delays
export default function () {
    http.get('/page1');
    sleep(randomBetween(2, 5));  // User reads page
    http.get('/page2');
    sleep(randomBetween(1, 3));  // User thinks
    http.get('/page3');
}

// ❌ MISTAKE 3: Single endpoint test only
export default function () {
    http.get('/api/products');  // Only this, repeatedly
}
// Real users hit multiple endpoints!

// ✅ FIX: Full user journey
export default function () {
    http.get('/');                    // Homepage
    sleep(2);
    http.get('/products');            // Browse
    sleep(3);
    http.get('/products/123');        // View detail
    sleep(2);
    http.post('/cart/add', {...});    // Add to cart
    sleep(1);
    http.get('/checkout');            // Checkout
}

// ❌ MISTAKE 4: Ignoring ramp-up
export const options = {
    vus: 1000,        // Langsung 1000 users
    duration: '5m',
};
// Sudden spike bukan typical traffic pattern

// ✅ FIX: Gradual ramp-up
export const options = {
    stages: [
        { duration: '2m', target: 100 },   // Warm up
        { duration: '5m', target: 500 },   // Ramp up
        { duration: '10m', target: 1000 }, // Peak
        { duration: '2m', target: 0 },     // Ramp down
    ],
};

// ❌ MISTAKE 5: Not checking response content
export default function () {
    const res = http.get('/api/users');
    check(res, {
        'status is 200': (r) => r.status === 200,
    });
    // Server bisa return 200 dengan empty/error body!
}

// ✅ FIX: Verify response content
export default function () {
    const res = http.get('/api/users');
    check(res, {
        'status is 200': (r) => r.status === 200,
        'has users': (r) => {
            const body = JSON.parse(r.body);
            return body.data && body.data.length > 0;
        },
        'response time OK': (r) => r.timings.duration < 2000,
    });
}

Optimization vs Scaling — Kapan Pilih Yang Mana?

DECISION FRAMEWORK:

                    ┌─────────────────────────┐
                    │  Website lambat/crash   │
                    │  di load test           │
                    └───────────┬─────────────┘
                                │
                    ┌───────────▼─────────────┐
                    │  Cek: Apakah server     │
                    │  resources maxed out?   │
                    │  (CPU/RAM > 80%)        │
                    └───────────┬─────────────┘
                                │
                ┌───────────────┴───────────────┐
                │                               │
               YES                              NO
                │                               │
                ▼                               ▼
    ┌───────────────────────┐     ┌───────────────────────┐
    │  Check: Code sudah    │     │  OPTIMIZE CODE FIRST  │
    │  optimal?             │     │  • Fix N+1 queries    │
    └───────────┬───────────┘     │  • Add caching        │
                │                 │  • Async processing   │
        ┌───────┴───────┐         │  • Better indexes     │
        │               │         └───────────────────────┘
       YES             NO
        │               │
        ▼               ▼
┌───────────────┐ ┌───────────────────────┐
│ SCALE UP/OUT  │ │  OPTIMIZE FIRST,      │
│ • Bigger      │ │  THEN SCALE           │
│   server      │ │                       │
│ • More        │ │  Scaling tanpa        │
│   instances   │ │  optimize = throwing  │
│ • Load        │ │  money at the problem │
│   balancer    │ └───────────────────────┘
└───────────────┘

RULE OF THUMB:
• Optimize dulu sampai code tidak bisa lebih efisien
• Baru kemudian scale kalau memang butuh more resources
• Scaling without optimization = expensive band-aid

"Jangan pernah scale sebelum optimize. Scaling aplikasi yang tidak optimal itu seperti beli mobil lebih besar karena mobil lama terlalu banyak sampah. Buang sampahnya dulu, baru evaluate apakah perlu mobil lebih besar."


Bagian 9: CI/CD Integration — Automated Load Testing

Untuk prevent regression, integrate k6 ke CI/CD pipeline. Setiap deploy, otomatis test.

GitHub Actions Workflow

# .github/workflows/load-test.yml

name: Load Testing

on:
  # Run on push to main/staging
  push:
    branches: [main, staging]

  # Run on PR to main
  pull_request:
    branches: [main]

  # Scheduled: Every day at 2 AM
  schedule:
    - cron: '0 2 * * *'

  # Manual trigger
  workflow_dispatch:
    inputs:
      test_type:
        description: 'Type of test to run'
        required: true
        default: 'smoke'
        type: choice
        options:
          - smoke
          - load
          - stress

jobs:
  smoke-test:
    name: Smoke Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install k6
        run: |
          curl -L <https://github.com/grafana/k6/releases/download/v0.50.0/k6-v0.50.0-linux-amd64.tar.gz> | tar xz
          sudo mv k6-v0.50.0-linux-amd64/k6 /usr/local/bin/
          k6 version

      - name: Run Smoke Test
        run: |
          k6 run \\
            -e BASE_URL=${{ secrets.STAGING_URL }} \\
            tests/smoke-test.js

      - name: Upload Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: smoke-test-results
          path: results/

  load-test:
    name: Load Test
    runs-on: ubuntu-latest
    needs: smoke-test  # Only run if smoke test passes
    if: github.event_name == 'push' || github.event.inputs.test_type == 'load'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install k6
        run: |
          curl -L <https://github.com/grafana/k6/releases/download/v0.50.0/k6-v0.50.0-linux-amd64.tar.gz> | tar xz
          sudo mv k6-v0.50.0-linux-amd64/k6 /usr/local/bin/

      - name: Run Load Test
        run: |
          k6 run \\
            -e BASE_URL=${{ secrets.STAGING_URL }} \\
            --out json=results/load-test.json \\
            --summary-export=results/summary.json \\
            tests/load-test.js

      - name: Check Results
        run: |
          # Parse summary and check for failures
          if grep -q '"fails":' results/summary.json; then
            FAILS=$(cat results/summary.json | jq '.metrics.checks.fails')
            if [ "$FAILS" -gt 0 ]; then
              echo "❌ Load test had $FAILS failed checks"
              exit 1
            fi
          fi
          echo "✅ Load test passed"

      - name: Upload Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: load-test-results
          path: results/

  notify-on-failure:
    name: Notify on Failure
    runs-on: ubuntu-latest
    needs: [smoke-test, load-test]
    if: failure()

    steps:
      - name: Send Slack Notification
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "⚠️ Load Test Failed!"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Repository:* ${{ github.repository }}\\n*Branch:* ${{ github.ref_name }}\\n*Commit:* ${{ github.sha }}\\n*Author:* ${{ github.actor }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Performance regression detected! Check the workflow for details."
                  }
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Workflow"
                      },
                      "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

Test Organization

Organize tests dengan struktur yang jelas:

tests/
├── smoke-test.js           # Basic sanity check (1-5 VUs, 1 min)
├── load-test.js            # Normal load (100 VUs, 10 min)
├── stress-test.js          # Find breaking point (ramp to 2000 VUs)
├── spike-test.js           # Sudden traffic spike
├── soak-test.js            # Long duration (1000 VUs, 2 hours)
├── scenarios/
│   ├── homepage.js         # Homepage specific tests
│   ├── search.js           # Search functionality
│   ├── checkout.js         # Checkout flow
│   └── api.js              # API endpoints
└── utils/
    ├── helpers.js          # Common helper functions
    └── data.js             # Test data generators

Environment-Specific Configuration

// tests/config.js

const environments = {
    local: {
        baseUrl: '<http://localhost:8000>',
        thresholds: {
            http_req_duration: ['p(95)<1000'],  // Relaxed for local
        },
    },
    staging: {
        baseUrl: '<https://staging.yourapp.com>',
        thresholds: {
            http_req_duration: ['p(95)<2000'],
        },
    },
    production: {
        baseUrl: '<https://yourapp.com>',
        thresholds: {
            http_req_duration: ['p(95)<1500'],  // Stricter for production
            http_req_failed: ['rate<0.005'],    // Less than 0.5% errors
        },
    },
};

export function getConfig() {
    const env = __ENV.TEST_ENV || 'staging';
    return environments[env];
}

// Usage in test:
import { getConfig } from './config.js';

const config = getConfig();
export const options = {
    thresholds: config.thresholds,
};

const BASE_URL = config.baseUrl;

Best Practice untuk CI/CD

CI/CD LOAD TESTING STRATEGY:

┌─────────────────────────────────────────────────────────────┐
│                    PR / Feature Branch                      │
├─────────────────────────────────────────────────────────────┤
│  • Run: Smoke Test only                                     │
│  • Duration: 1-2 minutes                                    │
│  • Purpose: Quick sanity check                              │
│  • Block merge: If smoke test fails                         │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Merge to Staging                         │
├─────────────────────────────────────────────────────────────┤
│  • Run: Smoke Test + Load Test                              │
│  • Duration: 10-15 minutes                                  │
│  • Purpose: Verify no performance regression                │
│  • Alert: If thresholds crossed                             │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Before Production Deploy                 │
├─────────────────────────────────────────────────────────────┤
│  • Run: Full suite (Smoke + Load + Stress)                  │
│  • Duration: 30-60 minutes                                  │
│  • Purpose: Full performance validation                     │
│  • Block deploy: If any test fails                          │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Scheduled (Daily/Weekly)                 │
├─────────────────────────────────────────────────────────────┤
│  • Run: Soak Test                                           │
│  • Duration: 2-4 hours                                      │
│  • Purpose: Detect memory leaks, long-term issues           │
│  • Alert: If degradation detected                           │
└─────────────────────────────────────────────────────────────┘

Bagian 10: Closing — Jangan Jadi Seperti Fajar (yang Versi Panik)

Recap: Yang Sudah Kita Pelajari

Dari case study Fajar, ini key takeaways:

LOAD TESTING ESSENTIALS:

1. TEST EARLY, TEST OFTEN
   └── Jangan tunggu H-3 launch
   └── Integrate ke development workflow

2. UNDERSTAND YOUR LIMITS
   └── Tau berapa concurrent users bisa di-handle
   └── Tau dimana breaking point
   └── Plan for scale before you need it

3. IDENTIFY BOTTLENECKS SYSTEMATICALLY
   └── k6 tunjukkan symptoms
   └── Code review untuk root cause
   └── Fix prioritas: highest impact first

4. COMMON ISSUES TO WATCH
   └── N+1 queries (paling sering!)
   └── Synchronous blocking calls
   └── No caching
   └── Connection pool exhaustion

5. OPTIMIZE BEFORE SCALE
   └── Efficient code > bigger server
   └── Caching > more database replicas
   └── Queue > more PHP workers

6. AUTOMATE
   └── CI/CD integration
   └── Scheduled tests
   └── Alert on regression

7. REALISTIC TESTING
   └── Production-like environment
   └── Production-like data
   └── Real user behavior patterns

Quick Reference: k6 Commands

# Basic run
k6 run test.js

# With virtual users and duration
k6 run --vus 100 --duration 5m test.js

# With environment variables
k6 run -e BASE_URL=https://staging.app.com test.js

# Save results to JSON
k6 run --out json=results.json test.js

# Export summary
k6 run --summary-export=summary.json test.js

# Multiple outputs
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 test.js

# Run specific scenario
k6 run --scenario smoke test.js

# With web dashboard (k6 v0.49+)
k6 run --out web-dashboard test.js

Rekomendasi Kelas di BuildWithAngga

Untuk menguasai building production-ready Laravel applications yang bisa handle high traffic, saya rekomendasikan beberapa kelas:

KelasYang DipelajariRelevansi
Laravel AdvancedOptimization, caching, queue, events, testingSemua yang Fajar butuhkan untuk fix issues
Laravel PerformanceQuery optimization, profiling, database tuningDeep-dive ke bottleneck fixing
Laravel API MasterclassRESTful API, rate limiting, authentication, cachingBuild APIs yang scalable
DevOps untuk LaravelDocker, CI/CD, monitoring, scalingInfrastructure & deployment

Kenapa Kelas-Kelas Ini:

  • Production-focused — Bukan sekadar tutorial CRUD
  • Real-world problems — Case study seperti Fajar
  • Complete implementation — Source code lengkap
  • Performance mindset — Dari awal sudah think about scale
  • Lifetime access — Belajar sesuai pace kamu
  • Community support — Diskusi dengan sesama developer

Quick Links:

👉 Kelas Laravel Advanced: buildwithangga.com/kelas/laravel-advanced

👉 Semua Kelas Laravel: buildwithangga.com/courses?category=laravel

👉 Kelas Gratis: buildwithangga.com/courses?type=free

Final Message

Load testing bukan luxury. Load testing adalah necessity.

Setiap website yang akan launch, setiap feature besar yang akan deploy, setiap expected traffic spike — semua butuh load test.

k6 is free. One hour learning = lifetime skill. Downtime is expensive. One hour down = reputation damage.

Don't be like Fajar — yang versi panik di H-3.

Be like Fajar — yang versi sekarang, yang selalu load test sebelum launch.

Start simple:

  1. Install k6 (5 menit)
  2. Write smoke test untuk homepage (10 menit)
  3. Run dan lihat hasilnya (2 menit)
  4. Expand ke endpoints lain (gradual)
  5. Integrate ke CI/CD (when ready)

Your future self — dan your future clients — akan berterima kasih.


Artikel ini ditulis oleh Angga Risky Setiawan, AI Product Engineer & Founder BuildWithAngga. Berdasarkan konsultasi nyata dengan freelancer yang menghadapi launch day crisis.

Punya pertanyaan tentang load testing, Laravel performance, atau scaling? Join komunitas BuildWithAngga dan diskusi dengan ribuan developer Indonesia!

👉 buildwithangga.com