Dua aturan yang menjelaskan hampir semuanya
Pertanyaanmu — kalau ada pod yang jalan, foto/video baru itu reuse atau queue — jawabannya keduanya, dan itu bukan kontradiksi. Dua hal yang berbeda sedang dibagi:
Pod dipakai ulang. Satu pod melayani seluruh sesi. Tidak ada pod baru
per klip, tidak ada cold start per klip. Aplikasi hanya tahu satu nama host
(POD_SSH_HOST) dan mengirim semua job ke sana.
Job dijalankan satu per satu. Worker dikunci di concurrency 1 — bukan karena sulit dinaikkan, tapi karena satu job GPU sudah menghabiskan seluruh kartu. Job kedua mengantre, dan halaman job menampilkan berapa yang ada di depannya.
Ada satu konsekuensi yang tidak kelihatan dan mahal: bobot model
dimuat ulang setiap job. pod_runner.py dijalankan sebagai
proses Python baru untuk tiap job, jadi cache model tidak pernah bertahan. Itu
~3,5 menit yang dibayar berulang. Pod yang menyala tetap jauh lebih murah daripada
serverless — ia menghemat penyewaan dan unduhan 77 GB, bukan pemuatan
bobotnya.
Peta besar
Semua yang melintasi batas rumah berawal dari rumah. Nol port terbuka, nol webhook, nol autentikasi worker. Pod tidak pernah menghubungi homelab — homelab yang bertanya.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','tertiaryColor':'#16332F','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
flowchart LR
subgraph HOME["Homelab (di balik VPN)"]
direction TB
B["Browser<br/>10.10.0.1:3100"]
A["FastAPI + worker<br/>satu proses"]
DB[("SQLite<br/>app.db")]
M[("media/<br/>input + hasil")]
B --> A
A --- DB
A --- M
end
subgraph RP["RunPod (disewa per sesi)"]
direction TB
P["pod A40<br/>/workspace"]
W["pod_runner.py<br/>+ handler.py"]
P --- W
end
A -- "scp input" --> P
A -- "spec.json via stdin" --> P
A -- "poll: ada result.json?" --> P
P -. "scp hasil" .-> A
Panah keluar semuanya dari homelab. Pod tidak punya cara memanggil balik.
Siklus hidup satu job
Status disimpan di jobs.status, tahap detailnya di
jobs.stage. Yang menentukan perilaku recovery bukan status, melainkan
apakah provider_job_id sudah terisi.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
stateDiagram-v2
[*] --> queued: POST /jobs lolos semua pagar
queued --> running: worker claim_next
queued --> cancelled: batal, gratis
running --> done: result.json ada, hasil terunduh
running --> failed: galat, atau dipanen reaper
running --> cancelled: batal, GPU dihentikan
failed --> queued: retry penuh, bayar lagi
failed --> running: retry unduh, gratis
done --> [*]
cancelled --> [*]
Tahap di dalam running berurutan:
preparing (ffmpeg di homelab) → uploading (scp) →
generating (GPU) → downloading (scp balik).
Kenapa tahap itu penting. Gagal di downloading berarti
generate-nya sudah selesai dan sudah dibayar — mengulang tarikannya
gratis. Gagal di tahap lain berarti harus dari nol, dan itu berbayar penuh. UI
membedakan keduanya dengan tombol yang berbeda.
Boot pertama kali
Tidak ada skrip boot pertama, dan memang tidak dibutuhkan.
Skema database dibuat sendiri saat koneksi pertama — db.connect()
menjalankan create table if not exists lalu menambal kolom yang lahir
belakangan lewat daftar MIGRATIONS. Menjalankannya dua kali aman.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant D as docker compose
participant L as lifespan
participant DB as SQLite
participant PH as pod_health
participant W as worker.loop
D->>L: start container
L->>L: mkdir MEDIA_ROOT
L->>DB: connect
DB-->>L: skema dibuat, kolom ditambal
alt POD_SSH_HOST terisi
L->>PH: probe pod, DITUNGGU
PH-->>L: terjangkau atau tidak
Note over L,PH: Ditunggu, bukan dilempar ke latar.<br/>Halaman pertama harus sudah tahu,<br/>kalau tidak job bisa lahir dengan<br/>provider yang salah dan itu permanen.
end
L->>W: create_task
W->>DB: recover_on_boot
W->>W: sweep_inbox, buang staging yatim
W->>W: spawn reaper + pemantau pod
W->>W: loop utama menunggu job
Yang tetap harus disiapkan tangan, sekali:
/home/v-pipeline/deploy/.env— minimalPOD_SSH_HOST,MONTHLY_BUDGET_USD,POD_USD_PER_SEC.docker-compose.ymldengan mount~/.sshread-only: config, kunci privat, known_hosts. Containerpython:3.12-slimtidak membawa klien SSH dan tidak melihat~/.sshmilik host.- Di pod:
handler.pydanpod_runner.pyharus ada di/workspace/, lalupod_bringup.shdijalankan.
Job baru, pod hidup
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant U as Browser
participant A as FastAPI
participant DB as SQLite
participant W as worker
participant P as pod
U->>A: POST /jobs foto + video
A->>A: pagar disk, anggaran, ukuran, durasi
A->>DB: create_job status=queued, provider=pod
A-->>U: HX-Redirect ke /jobs/id
Note over U,A: Provider DICAP di baris job.<br/>Mengganti cookie sesudah ini<br/>tidak mengubah job yang sudah lahir.
W->>DB: claim_next, jadi running
W->>W: ffmpeg normalisasi 30 fps, sisi 720
W->>P: scp input.png dan drive.mp4
W->>P: tulis spec.json lewat stdin
W->>P: wc -c spec.json, pastikan utuh
W->>P: setsid pod_runner.py
W->>DB: simpan provider_job_id SEBELUM apa pun
Note over W,DB: Ini satu-satunya hal yang mencegah<br/>tagihan ganda setelah reboot.
loop tiap 10 detik
W->>P: cat result.json
P-->>W: belum ada
end
P->>P: preprocessing, encode prompt, lepas text encoder
P->>P: muat 34 GB, denoising per segmen
P->>P: tulis result.json.part lalu rename
W->>P: cat result.json
P-->>W: selesai + output + detik
W->>DB: catat biaya SEBELUM unduh
W->>P: scp hasil ke media/id/
W->>DB: mark_done
U->>A: polling htmx 5 detik
A-->>U: fragmen hasil, polling berhenti sendiri
Kenapa spec.json lewat stdin, bukan argumen shell. Prompt
bisa memuat kutipan dan karakter apa pun. Meng-escape-nya ke baris perintah
adalah cara paling mudah membuat job gagal karena alasan yang tidak ada
hubungannya dengan model. Dan pengirimannya dipisah dari perintah menjalankan
runner: & di ujung rangkaian akan membackground seluruh
rangkaian termasuk cat yang membaca stdin — berkasnya jadi
0 byte sementara ssh tetap keluar dengan status 0.
Job kedua menyusul saat yang pertama masih jalan
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant U as Browser
participant DB as SQLite
participant W as worker
participant P as pod
Note over W,P: Job A sedang generating di pod
U->>DB: POST /jobs, job B lahir queued
U->>DB: baca queue_position job B
DB-->>U: 1 job di depan
Note over U: Halaman B menampilkan<br/>"Ada 1 job lain di depan"
W->>P: masih poll job A
P-->>W: job A selesai
W->>DB: job A done
W->>DB: claim_next ambil job B
W->>P: scp input job B ke pod yang SAMA
Note over W,P: Pod tidak dibuat ulang.<br/>Tapi handler dijalankan sebagai<br/>proses baru, jadi 34 GB bobot<br/>dimuat ULANG, ~3,5 menit.
W->>P: setsid pod_runner.py job B
Yang paling sering disalahpahami. Job yang sudah punya
provider_job_id selalu didahulukan di tiap putaran worker
(_pick_resumable), sebelum job antre mana pun. Alasannya benar: ia
sedang berjalan di GPU dan sudah dibayar. Tapi itu juga berarti satu job yang
menggantung akan menahan seluruh antrean — lihat skenario berikutnya.
Tidak ada pod sama sekali
Pod disewa per sesi dan dimatikan setelah selesai, jadi hampir sepanjang waktu
POD_SSH_HOST menunjuk host yang sudah tidak ada. Memeriksa variabel
env saja tidak cukup — itu membuat tombol POD tampil siap padahal setiap job
yang lahir dari sana pasti gagal.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant PH as pemantau pod
participant C as config
participant U as Browser
loop tiap 60 detik
PH->>PH: ssh -o ConnectTimeout=6 host true
PH->>C: simpan hasil ke cache
end
U->>C: buka halaman
C->>C: pod_blocker cek env DAN keterjangkauan
C-->>U: tombol POD MATI + alasannya
Note over U,C: "Pod tidak menjawab di SSH.<br/>Buat pod baru; tombol ini menyala<br/>sendiri dalam satu menit."
U->>C: cookie masih berisi "pod"
C-->>U: provider_of memaksa turun ke mock
Note over U,C: Kesiapan adalah fakta server dan<br/>MENGALAHKAN cookie. Mencegah<br/>perpindahan saja tidak menolong<br/>orang yang sudah terlanjur di sana.
Begitu kamu membuat pod baru dan memperbarui ~/.ssh/config, tombol
POD menyala sendiri dalam satu menit. Tidak perlu mengedit .env, tidak
perlu deploy ulang.
Pod mati di tengah job
Gangguan SSH bukan kegagalan job — koneksi yang putus
sesaat tidak berarti GPU berhenti bekerja. Jadi poll() sengaja
menjawab "masih running" saat SSH gagal. Itu keputusan yang benar, tapi ia
menciptakan satu lubang.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#E58873','lineColor':'#8C9B98','noteBkgColor':'#35211D','noteTextColor':'#EEBCAF','noteBorderColor':'#E58873','actorBkg':'#1C2429','actorBorder':'#E58873','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#35211D','labelBoxBorderColor':'#E58873','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant W as worker (_run_job)
participant R as reaper (task sendiri)
participant DB as SQLite
participant P as pod
W->>P: poll result.json
P--xW: connection refused, pod dihapus
W->>W: anggap masih running, tidur 10 detik
W->>DB: baca ulang status job
DB-->>W: masih running, lanjut poll
loop selamanya, sampai ada yang mengubah status
W->>P: poll
P--xW: refused
end
R->>DB: reap_stuck, ambang per provider
DB-->>R: job pod lewat 120 menit
R->>DB: mark_failed
W->>DB: baca ulang status
DB-->>W: bukan running lagi
W->>W: berhenti memantau, antrean jalan lagi
Bug yang pernah ada di sini, dan biayanya seluruh antrean. Reaper dulu
dipanggil di badan loop job, jadi ia hanya dapat giliran di antara dua
job. Ketika _run_job berputar tanpa ujung seperti di atas, reaper
— satu-satunya yang bisa membebaskannya — tidak pernah jalan.
Kuncinya melingkar: yang membebaskan job macet terkunci oleh job macet itu
sendiri. Dan karena _pick_resumable mengembalikan job yang sama
lebih dulu setiap kali, job baru tidak pernah dijemput. Ditemukan dengan
mengirim job mock dan melihatnya diam di queued satu menit penuh
padahal MOCK_SECONDS=15. Sekarang reaper hidup di task sendiri.
Ambangnya per provider, bukan satu angka. Satu klip di pod bisa lewat satu jam, jadi 45 menit akan menandai job gagal padahal GPU masih bekerja dan masih ditagih. Sekarang: mock 10 menit, pod 120 menit, runpod 45 menit.
Homelab restart di tengah job
Ini alasan status dibaca dari keberadaan berkas di pod, bukan dari proses SSH yang menggantung. Pod tidak peduli homelab-nya mati.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant A as aplikasi
participant DB as SQLite
participant P as pod
Note over A,P: Job running, provider_job_id sudah tersimpan
A--xA: homelab mati
Note over P: pod_runner jalan terus.<br/>Ia dijalankan dengan setsid,<br/>jadi tidak ikut mati saat<br/>koneksi SSH putus.
P->>P: selesai, tulis result.json ke disk pod
A->>A: container hidup lagi, restart unless-stopped
A->>DB: recover_on_boot
alt provider_job_id ADA
DB-->>A: resumed_count += 1, tetap running
Note over A,DB: LANJUT POLL. Tidak pernah<br/>disubmit ulang.
else provider_job_id KOSONG
DB-->>A: kembalikan ke queued
Note over A,DB: Belum sempat terkirim,<br/>aman diulang dari nol.
end
A->>P: poll result.json
P-->>A: sudah ada, selesai
A->>P: scp hasil
A->>DB: mark_done
Note over A,DB: UI menampilkan banner<br/>"Server sempat restart,<br/>job disambung, bukan dikirim ulang"
Unduhan gagal setelah generate selesai
Uangnya sudah keluar saat poll() mengembalikan done, bukan
saat berkasnya sampai. Karena itu biaya dicatat sebelum unduh, dan
pengulangan unduhan tidak boleh mencatat biaya kedua.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant W as worker
participant DB as SQLite
participant P as pod
participant U as Browser
P-->>W: done
W->>DB: record_cost, dijaga agar sekali saja
W->>DB: stage = downloading
W->>P: scp hasil, 3 percobaan
P--xW: gagal terus
W->>DB: mark_failed, stage tetap downloading
U->>DB: tekan Coba lagi
DB->>DB: requeue keep_provider_job = true
Note over DB: provider_job_id DIPERTAHANKAN.<br/>Tanpa itu job digenerate ulang<br/>dan dibayar dua kali.
W->>P: scp lagi
P-->>W: berhasil
W->>DB: _charge dipanggil lagi, tapi cost_usd<br/>sudah terisi jadi dilewati
W->>DB: mark_done
Membatalkan
Membatalkan job yang berjalan benar-benar menghentikan GPU dan memotong sisa waktunya. Yang sudah berjalan tetap ditagih. Kalau perintah berhenti itu gagal, job tidak ditandai cancelled — status yang berbohong ke arah "sudah berhenti" adalah yang paling mahal di sini.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#D9A65C','lineColor':'#8C9B98','noteBkgColor':'#33291A','noteTextColor':'#E8CFA4','noteBorderColor':'#D9A65C','actorBkg':'#1C2429','actorBorder':'#D9A65C','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#33291A','labelBoxBorderColor':'#D9A65C','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant U as Browser
participant A as FastAPI
participant P as pod
participant DB as SQLite
U->>A: POST /jobs/id/cancel
alt status queued
A->>DB: mark_cancelled, gratis
else status running
A->>P: pkill -f jobs/id
alt berhasil
P-->>A: ok
A->>DB: mark_cancelled
else gagal
P--xA: galat
A->>DB: mark_failed dengan peringatan
Note over A,DB: "Job kemungkinan MASIH BERJALAN<br/>dan masih ditagih. Hentikan lewat<br/>dasbor RunPod."
end
end
Pagar yang menolak job sebelum ia lahir
| Pagar | Ambang | Kenapa ada |
|---|---|---|
| Sisa disk | MIN_FREE_GB = 5 | Kalau root penuh yang ikut mati bukan cuma aplikasi ini, tapi Postgres, MySQL, dan uptime-kuma di mesin yang sama. |
| Anggaran bulanan | MONTHLY_BUDGET_USD = 25 | Berlaku untuk semua provider di db.BILLED, bukan runpod saja. Pod juga uang sungguhan. |
| Durasi video | MAX_VIDEO_SECONDS = 10 | Waktu generate tumbuh sebanding jumlah frame. Klip 10 detik sudah ~28 menit. |
| Ukuran berkas | 50 MB video, 10 MB foto | Pembacaan dihentikan di tengah body, jadi berkas 500 MB tidak pernah sampai utuh ke disk. |
| Prompt | wajib di edit | Di animate dan replace opsional — tapi mengosongkannya membuat model menebak rambut dan pakaian sendiri. |
| Kesiapan provider | blocker_for() | Provider yang tidak siap tampil mati beserta alasannya, dan cookie yang menunjuk ke sana dipaksa turun ke mock. |
Pod mati sendiri saat tidak ada yang memakainya
Sebelumnya pod tidak pernah mati sendiri. Satu-satunya
pengaman adalah --terminate-after, dan itu plafon jam dinding, bukan
deteksi idle — kalau kamu selesai lalu lupa, pod menagih sampai batas itu.
Sekarang aplikasi memeriksa kesepian dari database dan menghapus podnya sendiri. Ini satu-satunya tindakan merusak yang dijalankan tanpa diminta, jadi syaratnya sengaja banyak.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#D9A65C','lineColor':'#8C9B98','noteBkgColor':'#33291A','noteTextColor':'#E8CFA4','noteBorderColor':'#D9A65C','actorBkg':'#1C2429','actorBorder':'#D9A65C','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#33291A','labelBoxBorderColor':'#D9A65C','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant I as pengawas idle
participant DB as SQLite
participant RP as API RunPod
participant PH as pemantau pod
participant U as UI
loop tiap 60 detik
I->>DB: has_live, ada queued atau running?
alt masih ada job
DB-->>I: ya
Note over I,DB: Diam. Tidak peduli sudah<br/>berapa lama sepi sebelumnya.
else antrean benar-benar kosong
DB-->>I: tidak ada
I->>DB: last_activity, finished_at terakhir
I->>I: sepi = sekarang - max(finished_at, waktu boot)
alt sepi kurang dari ambang
Note over I: tunggu
else lewat ambang dan POD_ID terisi
I->>RP: podTerminate POD_ID
RP-->>I: terhapus
I->>PH: probe ulang segera
PH-->>U: tombol POD mati, ada alasannya
end
end
end
| Syarat | Default | Kenapa ada |
|---|---|---|
POD_ID terisi | kosong | Tanpa ini fitur mati total. POD_SSH_HOST cuma alias di
~/.ssh/config dan tidak memberi tahu pod mana yang harus dihapus.
Mencocokkan lewat IP bekerja sampai suatu hari ada dua pod dan yang salah
terhapus. Satu-satunya tindakan merusak di aplikasi ini tidak boleh menebak
sasarannya. |
POD_IDLE_MINUTES | 10 | 0 mematikan fitur. |
| Tidak ada job hidup | queued + running | Job yang masih mengantre pun menahan penghapusan — ia butuh podnya beberapa detik lagi. |
Hitung dari finished_at | bukan jam app | Kalau dihitung dari saat aplikasi menyadari sepi, setiap restart me-reset timernya dan pod menganggur bisa hidup berjam-jam. |
| Tenggang boot | 20 menit | Kamu baru membuat pod, menjalankan bringup, lalu men-deploy ulang aplikasi. Database kosong. Tanpa tenggang ini pod yang baru dibayar unduhan 77 GB-nya dihapus sepuluh menit kemudian. |
| Pod memang terjangkau | pod_health | Tidak mengirim perintah hapus berulang ke pod yang sudah tidak ada. |
Idle shutdown TIDAK menggantikan --terminate-after.
Keduanya melindungi dari kegagalan yang berbeda, dan yang kedua justru lebih
penting. Idle shutdown hanya bekerja kalau aplikasi ini hidup dan sehat —
saat homelab mati listrik, container crash dan tidak kembali, atau jaringan ke
RunPod putus, ia tidak pernah jalan sama sekali dan pod terus menagih.
--terminate-after ditegakkan RunPod, bukan oleh kita, jadi ia
selamat persis di saat kita yang gagal.
Aturannya: plafon biaya terakhirmu tidak boleh bergantung pada perangkat lunakmu sendiri sedang sehat. Buat batas keras itu longgar — 5 sampai 6 jam — supaya ia tidak pernah kena di pemakaian normal dan tidak pernah memotong job di tengah jalan. Yang benar-benar mengakhiri sesi tetap idle shutdown.
Menutup sesi, dan membuka yang berikutnya
Pod berhenti tetap menagih disknya. 150 GB yang diam berharga sekitar $15 per bulan. Akhiri sesi dengan menghapus pod, bukan menghentikannya.
%%{init:{'theme':'base','themeVariables':{'background':'#151B1E','primaryColor':'#1C2429','primaryTextColor':'#E7EEEC','primaryBorderColor':'#4FC3AC','lineColor':'#8C9B98','noteBkgColor':'#16332F','noteTextColor':'#BFE6DC','noteBorderColor':'#4FC3AC','actorBkg':'#1C2429','actorBorder':'#4FC3AC','actorTextColor':'#E7EEEC','signalColor':'#8C9B98','signalTextColor':'#E7EEEC','labelBoxBkgColor':'#16332F','labelBoxBorderColor':'#4FC3AC','labelTextColor':'#E7EEEC','loopTextColor':'#8C9B98','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
sequenceDiagram
autonumber
participant K as Kamu
participant RP as runpodctl
participant PH as pemantau pod
participant U as UI
K->>RP: pod create --terminate-after 3h --ssh
RP-->>K: id, ip, port
K->>RP: rp.sh pods, HITUNG jumlahnya
Note over K,RP: pod create bisa BERHASIL sambil<br/>keluar dengan exit code bukan nol.<br/>Mengulanginya buta pernah<br/>melahirkan pod kedua: $0.68 hangus.
K->>K: perbarui ~/.ssh/config host dan port
K->>RP: scp handler.py + pod_runner.py ke /workspace
K->>RP: jalankan pod_bringup.sh
PH->>PH: probe berikutnya, maks 60 detik
PH-->>U: tombol POD menyala sendiri
Note over K,U: --- sesi dipakai ---
K->>RP: HAPUS pod
PH->>PH: probe gagal
PH-->>U: tombol POD mati sendiri, ada alasannya
Angka yang terukur, bukan diperkirakan
Semuanya dari pod A40 48 GB, klip 6,8 detik mode replace, 480p, 20 langkah, 17 Agustus 2026.
| Tahap | Waktu | Catatan |
|---|---|---|
| Bringup pod dari nol | ~9 menit | Termasuk unduhan bobot 77 GB. Sekali per sesi. |
| Normalisasi + unggah | ~50 detik | ffmpeg di Athlon dua core, lalu scp. |
| Preprocessing replace | ~60 detik | Termasuk SAM2 untuk mask dan latar. |
| Muat model + lepas text encoder | ~3,5 menit | Dibayar setiap job, bukan sekali per sesi. |
| Generate | 16,2 dtk/langkah | Per segmen. 204 frame = 3 segmen = ~16 menit. |
| Total satu klip | ~22 menit | Biaya tercatat $0,1557. |
| Puncak VRAM | 44,8 / 48 GB | Bobot 34,2 GB + aktivasi 10,6 GB. |
| RAM container | 15 / 50 GB | Batas cgroup 50 GB, bukan 503 GB seperti laporan free. |
Rumus perkiraan yang dipakai UI. frame = durasi × 30 (preprocessing memaksa 30 fps apa pun masukannya) · segmen = frame ÷ 77 dibulatkan ke atas · generate = segmen × langkah × 16,2 detik. Ditambah preprocessing dan pemuatan model. Ia perkiraan dari satu titik ukur di satu kartu, dan UI menyebutnya perkiraan — angka yang terlihat pasti akan dipercaya.