Sistem Upload dengan Proses Berat dan Lama di Backend (Asynchronous)
12 min read

Sistem Upload dengan Proses Berat dan Lama di Backend (Asynchronous)

Banyak sistem enterprise punya kebutuhan yang mirip: user meng-upload file besar — bisa Excel, CSV, atau ZIP — yang kemudian harus diproses dengan logic yang tidak sederhana. Validasi data baris per baris, transformasi format, enrichment dengan data dari sumber lain, hingga penyimpanan akhir ke database. Proses semacam ini bukan hal yang selesai dalam hitungan detik; bisa memakan waktu beberapa menit, kadang lebih, tergantung ukuran file dan kompleksitas logic-nya. Ketika proses sebesar itu dipaksakan berjalan secara synchronous di dalam satu request HTTP, masalah muncul dari berbagai arah sekaligus. Artikel ini membahas mengapa pendekatan synchronous bukan pilihan yang masuk akal untuk kasus seperti ini, dan bagaimana arsitektur asynchronous processing dengan mekanisme monitoring status menjadi solusi yang lebih tepat.

Mengapa Synchronous Bukan Pilihan

Bayangkan sebuah endpoint upload yang menerima file, langsung memvalidasi setiap barisnya, mentransformasi datanya, dan menyimpannya ke database — semua dalam satu siklus request-response. Untuk file kecil, pendekatan ini mungkin terasa baik-baik saja. Tapi begitu ukuran file membesar atau logic pemrosesannya makin kompleks, tiga masalah muncul secara bersamaan.

Masalah pertama adalah request HTTP akan timeout. Load balancer, reverse proxy, API gateway, bahkan browser punya batas waktu tunggu yang jauh lebih singkat dari lima menit. Begitu batas itu terlampaui, koneksi diputus paksa, padahal di belakangnya proses mungkin masih berjalan dan resource masih terpakai untuk sesuatu yang hasilnya tidak akan pernah sampai ke user.

Masalah kedua adalah user experience yang sangat buruk. User dipaksa menatap halaman loading selama lima menit tanpa indikasi progress yang jelas. Tidak ada cara untuk mengetahui apakah proses berjalan normal, macet, atau sudah gagal di tengah jalan. Jika koneksi internet user putus sesaat, seluruh proses bisa hilang tanpa jejak meski sebenarnya backend masih bekerja.

Masalah ketiga adalah resource backend akan terkunci lama. Setiap request yang sedang diproses menahan satu slot dari worker pool HTTP server. Jika ada beberapa user yang upload file besar secara bersamaan, seluruh pool bisa terisi penuh hanya untuk menangani proses berat ini — dan request lain yang seharusnya ringan dan cepat ikut terdampak karena tidak ada worker yang tersisa untuk melayaninya.

PENDEKATAN SYNCHRONOUS UNTUK PROSES BERAT:
  ✗ Request HTTP timeout sebelum proses selesai
  ✗ User tidak tahu progress, hanya menatap loading
  ✗ Resource backend (worker pool, koneksi) terkunci lama
  ✗ Satu file besar bisa mempengaruhi performa seluruh API

Solusi yang tepat untuk situasi ini adalah arsitektur asynchronous processing, dilengkapi mekanisme monitoring status yang memungkinkan user memantau progress tanpa harus menunggu di halaman yang sama.


Gambaran Besar Solusi

Ide dasar dari arsitektur ini adalah memisahkan satu proses besar yang tadinya monolitik menjadi tiga komponen independen yang masing-masing punya tanggung jawab jelas.

flowchart LR
    A[Upload & Acceptance] -->|cepat| B[Heavy Processing]
    B -->|asynchronous, lama| C[Monitoring & Observability]
    C -->|status ke user| D[User]

Upload dan acceptance adalah tahap pertama yang harus berjalan cepat — menerima file dari user, menyimpannya ke storage, mencatatnya sebagai task yang menunggu diproses, lalu langsung mengirim response. Tahap ini seharusnya selesai dalam hitungan detik, tidak peduli seberapa berat proses yang menyusul setelahnya.

Heavy processing adalah tahap kedua yang berjalan di luar siklus request HTTP — inilah tempat seluruh logic berat (validasi, transformasi, enrichment, penyimpanan) benar-benar dieksekusi. Tahap ini bisa berjalan lama tanpa risiko timeout, karena tidak ada koneksi HTTP yang menunggu hasilnya secara langsung.

Monitoring dan observability adalah tahap ketiga yang memberi user visibility terhadap apa yang terjadi pada file mereka, tanpa perlu menunggu di tempat yang sama. User tidak perlu tahu detail teknis di balik layar; mereka hanya perlu tahu bahwa file sudah diterima, dan proses sedang berjalan, sudah selesai, atau gagal.

Filosofi inti dari arsitektur ini sederhana: pisahkan “menerima pekerjaan” dari “mengerjakan pekerjaan”. Begitu prinsip ini dipegang konsisten, banyak keputusan desain berikutnya — struktur data, pilihan worker, strategi error handling — menjadi lebih mudah diturunkan secara natural.

Flow Tingkat Tinggi

Secara garis besar, seluruh siklus hidup sebuah file yang di-upload mengikuti enam langkah berikut, dari saat user menekan tombol upload hingga hasilnya bisa dipantau.

sequenceDiagram
    participant User
    participant Backend
    participant Storage
    participant DB
    participant Worker

    User->>Backend: 1. Upload file
    Backend->>Storage: 2. Simpan file (object storage)
    Backend->>DB: 3. Catat task (queue-like table)
    Backend-->>User: 4. Response langsung
    Note over Worker: Berjalan independen
    Worker->>DB: 5. Proses task secara asynchronous
    Worker->>DB: 6. Update status
    User->>Backend: Cek status kapan saja

Langkah pertama dan keempat adalah satu-satunya bagian yang melibatkan user secara langsung dan harus berjalan cepat. User upload file, backend menyimpannya ke object storage, mencatatnya sebagai task baru di database yang berfungsi seperti tabel antrian, lalu langsung memberi response — semua ini idealnya selesai dalam hitungan detik. Setelah itu, worker atau processor yang berjalan independen mengambil task tersebut dan memprosesnya secara asynchronous, mengupdate status sepanjang prosesnya berjalan, sehingga user bisa memantau perkembangannya kapan saja tanpa perlu tetap terhubung di halaman yang sama.


Data Model

Satu tabel sederhana sudah cukup untuk merepresentasikan seluruh siklus hidup task upload, mulai dari diterima hingga selesai diproses.

Tabel upload_tasks umumnya berisi kolom id sebagai identifier unik, user_id untuk mengetahui siapa pemilik task, file_path yang menunjuk ke lokasi file di object storage, status yang merepresentasikan state saat ini (PENDING, PROCESSING, DONE, atau FAILED), progress sebagai angka 0 hingga 100 yang bersifat opsional untuk kasus di mana progress granular memang relevan ditampilkan, error_message yang terisi ketika status FAILED, serta created_at dan updated_at untuk keperluan audit dan analisis waktu proses.

stateDiagram-v2
    [*] --> PENDING: task dibuat
    PENDING --> PROCESSING: worker mengambil task
    PROCESSING --> DONE: proses berhasil
    PROCESSING --> FAILED: proses gagal
    DONE --> [*]
    FAILED --> [*]

Tabel ini sebenarnya melayani tiga fungsi sekaligus, meskipun strukturnya tunggal. Pertama, ia berfungsi sebagai antrian proses — worker mengambil baris dengan status PENDING untuk diproses berikutnya. Kedua, ia berfungsi sebagai sumber data monitoring — endpoint status cukup query tabel ini untuk memberi tahu user kondisi terkini file mereka. Ketiga, ia berfungsi sebagai audit dan history — kapan file di-upload, kapan mulai diproses, kapan selesai, dan jika gagal, apa alasannya, semua tersimpan secara natural tanpa perlu tabel tambahan.

Kolom progress bersifat opsional karena tidak semua jenis pemrosesan punya cara natural untuk menghitung persentase kemajuan. Untuk proses yang terdiri dari langkah-langkah diskret (misalnya parsing, lalu validasi, lalu simpan), progress granular relatif mudah dihitung. Untuk proses yang sifatnya satu blok besar, status biner PENDING/PROCESSING/DONE/FAILED biasanya sudah cukup informatif bagi user.

UX dan User Flow

Pengalaman pengguna dalam arsitektur asynchronous berbeda secara fundamental dari pendekatan synchronous, dan perbedaan ini harus tercermin jelas di antarmuka yang dirancang.

Saat Upload

Begitu user selesai memilih file dan menekan tombol upload, sistem seharusnya langsung menampilkan pesan singkat seperti “File berhasil di-upload dan sedang diproses” — bukan spinner yang berputar tanpa akhir yang jelas. Pesan ini secara implisit memberi tahu user bahwa mereka boleh meninggalkan halaman ini dan kembali nanti, karena prosesnya tidak terikat pada sesi browser yang sedang aktif.

Halaman Status

Halaman ini menampilkan daftar riwayat upload milik user, lengkap dengan status masing-masing file, waktu upload, dan waktu update terakhir. Halaman semacam ini memberi user kontrol untuk memeriksa banyak file sekaligus tanpa harus membuka satu per satu, terutama berguna ketika user melakukan beberapa upload dalam rentang waktu yang berdekatan.

Halaman Detail

Untuk satu file spesifik, halaman detail menampilkan status secara lebih granular — termasuk progress jika tersedia, dan yang paling penting, pesan error yang jelas ketika proses gagal. Pesan error yang informatif (bukan sekadar “terjadi kesalahan”) membantu user memahami apa yang perlu mereka perbaiki sebelum mencoba upload ulang, misalnya format kolom yang tidak sesuai atau data yang tidak valid di baris tertentu.

Jangan biarkan halaman status hanya menampilkan status mentah seperti PROCESSING tanpa konteks tambahan. User awam tidak selalu memahami arti istilah teknis ini — pertimbangkan menerjemahkannya ke bahasa yang lebih familiar, misalnya “Sedang diproses” disertai estimasi waktu jika tersedia, agar user tidak merasa sistem diam tanpa kabar.

Backend Processing — Dua Pendekatan

Setelah task tercatat di database, ada dua pendekatan umum untuk benar-benar mengeksekusi pemrosesan berat tersebut. Keduanya valid, dengan trade-off yang berbeda tergantung skala dan infrastruktur yang tersedia.

Opsi 1: CLI Worker (Running Forever)

Pendekatan pertama menggunakan program yang berjalan terus-menerus sebagai proses long-running, secara berkala memeriksa database untuk mencari task yang menunggu diproses.

flowchart TD
    A[Worker mulai loop] --> B{Ada task PENDING?}
    B -- Tidak --> C[Tunggu interval, misal 5 detik]
    C --> A
    B -- Ya --> D[Lock task, ubah ke PROCESSING]
    D --> E[Proses task]
    E --> F{Berhasil?}
    F -- Ya --> G[Update ke DONE]
    F -- Tidak --> H[Update ke FAILED]
    G --> A
    H --> A

Karakteristik utama dari pendekatan ini adalah programnya berjalan tanpa henti, melakukan query ke database setiap interval tertentu (misalnya setiap 5 detik), mengambil task berstatus PENDING, mengunci task itu dengan mengubah statusnya menjadi PROCESSING, lalu memproses dan mengakhirinya dengan status DONE atau FAILED.

Kelebihan pendekatan ini terletak pada kesederhanaannya. Program seperti ini relatif sederhana, bisa di-deploy sebagai container di EC2, VM, atau Cloud Run, dan sangat cocok untuk environment on-premise atau yang belum sepenuhnya cloud-native. Tidak ada dependency eksternal selain database yang memang sudah ada.

Namun ada kekurangan yang perlu dipertimbangkan. Polling secara inheren tidak efisien — worker terus bertanya ke database meski tidak ada task baru, menghabiskan sedikit resource bahkan saat idle. Scaling juga harus dilakukan secara manual; jika beban meningkat, kamu perlu menambah instance worker secara sadar, bukan otomatis. Dan ketika tidak ada task sama sekali dalam jangka waktu lama, resource yang dialokasikan untuk worker tetap terpakai tanpa menghasilkan kerja yang berguna.

Opsi 2: AWS SQS + Lambda (Event-Driven)

Pendekatan kedua membalik model di atas: daripada worker yang aktif bertanya, sistem upstream yang aktif memberi tahu begitu ada pekerjaan baru.

sequenceDiagram
    participant Backend
    participant SQS
    participant Lambda
    participant DB

    Backend->>SQS: Push message (1 task = 1 message)
    SQS->>Lambda: Trigger otomatis
    Lambda->>DB: Ambil & proses task
    Lambda->>DB: Update status
    Note over SQS,Lambda: Auto-scaling sesuai volume message

Karakteristik utamanya, saat upload terjadi, backend langsung mem-push message ke SQS, dengan setiap task direpresentasikan sebagai satu message. Lambda atau worker lain dipanggil secara otomatis ketika message tersedia, dan seluruh mekanisme ini scaling secara otomatis mengikuti volume message yang masuk.

Kelebihan pendekatan ini ada pada sifatnya yang benar-benar event-driven — tidak ada polling sama sekali, dan delay antara task dibuat dengan task mulai diproses jadi jauh lebih kecil. Scaling terjadi otomatis tanpa campur tangan manual, dan model ini sangat cost-efektif untuk workload yang tidak konstan, karena Lambda pada dasarnya hanya membebankan biaya saat benar-benar dijalankan.

Kekurangannya, pendekatan ini melibatkan lebih banyak komponen yang harus dipahami dan dikonfigurasi dengan benar. Kamu perlu memahami konsep retry, dead-letter queue (DLQ), dan visibility timeout — ketiganya krusial untuk memastikan task yang gagal tidak hilang begitu saja atau diproses berulang kali secara tidak terkendali.


EventBridge + Lambda sebagai Alternatif Hybrid

Selain dua pendekatan utama di atas, ada juga pendekatan hybrid yang menggabungkan elemen polling dengan infrastruktur serverless: EventBridge yang menjalankan cron job pada interval tertentu (misalnya setiap satu menit), memicu Lambda yang kemudian melakukan scan ke database untuk mencari task berstatus PENDING.

flowchart LR
    A[EventBridge cron, tiap 1 menit] --> B[Lambda]
    B --> C[Scan DB: cari task PENDING]
    C --> D[Proses task yang ditemukan]

Pendekatan ini cocok dipertimbangkan dalam beberapa kondisi spesifik: ketika kamu tidak ingin menambah kompleksitas SQS ke dalam stack, ketika rate kemunculan task relatif rendah sehingga delay satu menit dari interval cron tidak jadi masalah berarti, dan ketika arsitektur ingin dipertahankan tetap serverless tanpa harus menjalankan container yang hidup terus-menerus.

Namun secara prinsip, SQS tetap lebih tepat untuk use case yang memang berbentuk queue. EventBridge + Lambda pada dasarnya masih mengandung elemen polling — hanya saja polling itu dipindahkan dari level aplikasi ke level infrastruktur terjadwal. Untuk kasus di mana latensi rendah dan skalabilitas terhadap burst traffic benar-benar penting, SQS memberikan model yang lebih murni event-driven.

PendekatanModelCocok untuk
CLI WorkerPolling kontinuSkala kecil, on-premise, infrastruktur sederhana
SQS + LambdaEvent-driven murniSkala produksi, burst traffic, cost-sensitive
EventBridge + LambdaPolling terjadwalServerless tanpa SQS, task rate rendah

Error Handling dan Reliability

Sistem asynchronous, karena sifatnya berjalan di luar siklus request yang bisa langsung dipantau, butuh perhatian ekstra pada penanganan error agar task tidak “hilang” secara diam-diam tanpa jejak yang bisa diinvestigasi.

Beberapa best practice yang patut diterapkan konsisten meliputi: status PROCESSING harus bersifat atomic, biasanya dicapai dengan SELECT FOR UPDATE di level database, untuk memastikan tidak ada dua worker yang mengambil task yang sama secara bersamaan. Retry sebaiknya dibatasi jumlahnya, bukan diulang tanpa batas, agar task yang konsisten gagal tidak menghabiskan resource secara berulang. Setelah retry mencapai jumlah maksimum, task sebaiknya dipindahkan ke kondisi dead-letter — ditandai FAILED secara permanen — daripada terus dicoba ulang selamanya. Dan yang terakhir, error message harus disimpan dengan detail yang cukup untuk keperluan debugging, bukan sekadar flag boolean yang menandakan “gagal” tanpa konteks apapun.

RELIABILITY CHECKLIST:
  □ Status PROCESSING diubah secara atomic (SELECT FOR UPDATE / SKIP LOCKED)
  □ Retry dibatasi jumlahnya, tidak diulang tanpa batas
  □ Task yang gagal setelah N kali retry ditandai FAILED secara permanen
  □ Error message disimpan lengkap, bukan sekadar status gagal/berhasil
  □ Ada visibility (dashboard/log) untuk task yang stuck terlalu lama di PROCESSING
Tanpa mekanisme atomic locking saat mengambil task, dua worker yang berjalan bersamaan — baik itu dua instance CLI Worker atau dua invocation Lambda yang tumpang tindih — bisa memproses task yang sama persis secara paralel. Ini berisiko menghasilkan data ganda di database atau efek samping lain yang tidak diinginkan, terutama jika proses yang dijalankan tidak bersifat idempotent.

Kesimpulan

Arsitektur asynchronous untuk upload dengan proses berat bukan sekadar pilihan desain yang nice-to-have — untuk sistem modern dengan workload yang signifikan, pendekatan ini hampir bersifat mandatory. Tiga prinsip yang sudah dibahas di artikel ini saling melengkapi: pisahkan upload dari processing agar request HTTP tidak pernah menunggu proses berat, gunakan status tracking yang konsisten agar user dan sistem punya visibility yang sama terhadap progress, dan pilih model worker yang sesuai dengan skala kebutuhan kamu saat ini.

Untuk skala kecil dengan traffic yang relatif stabil dan dapat diprediksi, CLI Worker sudah cukup memadai tanpa perlu kompleksitas tambahan. Untuk skala produksi yang lebih besar dengan traffic yang fluktuatif, SQS dikombinasikan dengan Lambda menjadi pilihan yang lebih ideal karena sifatnya yang event-driven dan auto-scaling secara native.


Ringkasan

  • Synchronous processing gagal untuk proses berat karena tiga masalah sekaligus: request timeout, user experience buruk, dan resource backend terkunci lama.
  • Pisahkan tiga tanggung jawab: upload/acceptance yang cepat, heavy processing yang asynchronous, dan monitoring/observability yang memberi visibility ke user.
  • Flow tingkat tinggi: upload → simpan ke storage → catat task ke DB → response cepat → worker proses asynchronous → status terupdate dan bisa dipantau.
  • Satu tabel upload_tasks cukup berfungsi sebagai antrian proses, sumber data monitoring, sekaligus audit history.
  • CLI Worker (polling) cocok untuk skala kecil dan infrastruktur sederhana; SQS + Lambda (event-driven) cocok untuk skala produksi dengan traffic fluktuatif.
  • EventBridge + Lambda adalah alternatif hybrid yang tetap serverless tanpa SQS, tapi secara prinsip SQS lebih tepat untuk use case queue murni.
  • Reliability membutuhkan locking atomic, retry terbatas, dead-letter handling, dan error message yang informatif untuk debugging.
  • Pilihan worker model bergantung pada skala — tidak ada satu jawaban universal yang cocok untuk semua kondisi infrastruktur dan volume traffic.

Portofolio