Django vs Flask di VPS Kecil: RAM dan Worker
Bandingkan Django dan Flask pada VPS RAM 1 hingga 2 GB: lihat memori resident per worker gunicorn dan jumlah worker yang realistis dijalankan.
Biaya Django dan Flask pada VPS kecil
Django dan Flask pada VPS kecil terutama merupakan persoalan memori. Django memuat object relational mapper (ORM), mekanisme migrasinya, dan, jika diaktifkan, situs admin ke setiap proses worker yang Anda jalankan. Flask memuat router dan object request. Pada server dengan RAM 1 GB, perbedaan ini menentukan jumlah worker yang dapat dijalankan. Jumlah worker menentukan jumlah request yang dapat dilayani secara bersamaan.
Biaya tersebut hanya menjadi beban Django jika Anda tidak memanfaatkan kembali fitur yang disediakannya. Aplikasi dengan akun pengguna, session, dan panel admin lebih sesuai menggunakan Django. RAM per worker merupakan biaya untuk kode yang tidak perlu Anda tulis sendiri. API JSON di depan datastore yang sudah Anda jalankan lebih sesuai menggunakan Flask karena tidak ada fitur bawaan yang perlu dimuat. Ini adalah persoalan kesesuaian. Pengukuran di bawah ini menunjukkan posisi aplikasi Anda.
Berapa banyak memori yang digunakan satu worker gunicorn?
The data behind this chart
[
{
"label": "Bare Python 3.12 process",
"rss_mb": 14,
"pss_mb": 9
},
{
"label": "Flask, one route",
"rss_mb": 42,
"pss_mb": 26
},
{
"label": "Flask + SQLAlchemy",
"rss_mb": 58,
"pss_mb": 38
},
{
"label": "Django, admin disabled",
"rss_mb": 78,
"pss_mb": 47
},
{
"label": "Django, admin enabled",
"rss_mb": 96,
"pss_mb": 58
}
]Angka tersebut merupakan angka umum yang dipublikasikan untuk aplikasi hello world dengan setiap bentuk pada Ubuntu 24.04 menggunakan Python 3.12, tiga worker gunicorn, dan preload yang diaktifkan. Anggap angka ini sebagai batas bawah karena import aplikasi Anda sendiri akan menambah penggunaan memori. Worker Django dengan admin yang diaktifkan menggunakan 96 MB memori resident, sedangkan proporsi memorinya adalah 58 MB. Perbedaan antara kedua angka tersebut menjadi pokok bahasan di bagian berikutnya.
Lakukan pengukuran yang sama pada server Anda.
sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitleInstal setproctitle. Jika tersedia, gunicorn mengganti nama prosesnya menjadi gunicorn: master [site1] dan gunicorn: worker [site1]. Dengan demikian, perintah berikutnya dapat menemukan worker berdasarkan nama, bukan dengan menebak.
pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')Kolom rss adalah resident set size dalam kilobyte: setiap halaman memori yang saat ini ditahan proses di RAM. Menjumlahkannya di seluruh worker menghasilkan angka yang terlalu tinggi karena worker hasil fork berbagi halaman dengan proses induknya dan worker lain. Akibatnya, halaman yang sama dihitung beberapa kali. Sebagai gantinya, minta kernel memberikan proportional set size (PSS), yang membagi setiap halaman bersama di antara proses yang memetakannya.
for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; doneJalankan perintah tersebut sebagai user yang memiliki worker, atau gunakan sudo. PSS adalah kolom yang harus digunakan untuk menetapkan anggaran memori karena PSS menjumlahkan penggunaan secara tepat, sedangkan RSS tidak.
Django lebih besar karena fungsi django.setup(). Fungsi tersebut mengimpor setiap entri dalam INSTALLED_APPS, membangun application registry, dan membuat instance setiap kelas model beserta objek Python untuk setiap field di dalamnya. Penambahan django.contrib.admin menjalankan autodiscovery admin, yang mengimpor modul admin dari setiap app dan turut memuat layer form serta template. Worker Flask mengimpor Werkzeug dan Jinja2, lalu berhenti.
Ada satu catatan penting: framework sering kali hanya menggunakan sebagian kecil memori. Worker yang mengimpor cloud SDK atau pustaka untuk komputasi numerik dapat menggunakan lebih banyak memori daripada worker Django. Ukur aplikasi Anda yang sebenarnya sebelum menyimpulkan bahwa framework adalah masalahnya.
Copy on write dan alasan preload mengubah jumlahnya
Proses master Gunicorn melakukan fork terhadap worker. Tepat setelah fork(), proses anak berbagi setiap halaman memori dengan proses induk. Kernel hanya menyalin halaman ketika salah satu sisi menulis ke halaman tersebut. Jadi, apakah registri model Django ada satu kali atau empat kali di server bergantung pada sisi fork tempat registri tersebut dibuat.
Jika preload_app dinonaktifkan, setiap worker mengimpor aplikasi setelah proses tersebut di-fork. Akibatnya, setiap worker membuat salinan privatnya sendiri. Jika opsi tersebut diaktifkan, proses master mengimpor aplikasi satu kali, lalu worker mewarisi halaman-halaman tersebut.
import gc
bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50
def when_ready(server):
gc.freeze()CPython tidak bekerja optimal dengan copy on write. Setiap header objek menyimpan reference count. Mengakses objek akan menulis ke header tersebut, sehingga halaman yang dibagikan disalin kembali satu per satu saat garbage collector menelusuri heap. gc.freeze() memindahkan semua objek yang telah dialokasikan ke dalam generasi permanen yang tidak lagi dikunjungi collector. Hal ini membuat lebih banyak halaman tetap dibagikan. when_ready adalah hook yang tepat karena dijalankan setelah preload dan sebelum worker pertama di-fork. Ukur PSS sebelum dan sesudah menambahkannya, karena penghematannya bergantung pada seberapa banyak bagian aplikasi yang menjadi state saat import.
Preload memiliki satu biaya yang sering mengejutkan saat deployment. systemctl reload mengirim HUP, dan perilaku Gunicorn yang terdokumentasi pada HUP adalah memuat ulang konfigurasinya serta memulai worker baru. Saat aplikasi di-preload, Gunicorn tidak mengimpor ulang kode Anda. Jadi, release baru belum berjalan meskipun proses worker-nya baru. Gunakan systemctl restart setelah perubahan kode, atau jalankan urutan USR2 lalu WINCH jika worker lama harus menyelesaikan permintaan terlebih dahulu.
Berapa banyak worker yang secara realistis dapat dijalankan oleh VPS 1 GB?
The data behind this chart
[
{
"label": "Ubuntu 24.04 base",
"ram_mb": 190
},
{
"label": "nginx",
"ram_mb": 12
},
{
"label": "PostgreSQL, default config",
"ram_mb": 120
},
{
"label": "Headroom you must leave",
"ram_mb": 150
},
{
"label": "Left for gunicorn workers",
"ram_mb": 550
}
]Itu adalah angka saat idle pada server yang tidak melayani apa pun. Sekitar 550 MB tersisa untuk worker aplikasi, dan jumlah itu dihitung sebelum request pertama tiba.
Sekarang bagi, lalu gunakan perhitungan yang pesimistis. Sebuah request menggunakan memori selama diproses: queryset yang memuat beberapa ribu baris, lalu proses render template. Penggunaan puncak per worker biasanya mendekati dua kali angka saat idle, jadi gunakan dua kali lipat dalam perhitungan. Django dengan admin pada 58 MB saat idle memberi Anda empat worker pada server ini. Flask dengan SQLAlchemy pada 38 MB memberi Anda tujuh worker.
Saran (2 x cores) + 1 dari Gunicorn mengasumsikan bahwa CPU adalah sumber daya yang terbatas, sedangkan RAM bukan. Pada VPS kecil, kondisinya justru sebaliknya. Satu vCPU bersama juga memberi Anda kurang dari kapasitas kerja satu core ketika host sedang sibuk. Hal ini perlu dipahami sebelum Anda menyalahkan kode: CPU steal time dari tetangga yang terlalu banyak menggunakan resource muncul di top sebagai angka st.
Jika view Anda terutama menunggu database atau API upstream, thread lebih sesuai daripada proses dalam kondisi ini. --worker-class gthread --workers 2 --threads 4 memberi delapan request konkuren dengan penggunaan memori setara dua worker, karena thread berbagi satu salinan interpreter dan framework yang telah dimuat. Global interpreter lock berarti thread tidak membantu view yang menghabiskan CPU.
Sediakan swap untuk server tersebut. VPS 1 GB tanpa swap akan mengubah lonjakan penggunaan memori menjadi proses yang dihentikan, sedangkan swapfile mengubah lonjakan yang sama menjadi request yang lambat.
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --systemKemudian batasi aplikasi itu sendiri. MemoryMax=600M pada unit gunicorn membuat kernel mengambil kembali memori dari cgroup aplikasi Anda, bukan memilih proses yang dihentikan dari seluruh server. Dengan demikian, request yang menggunakan resource secara tidak terkendali tidak ikut memutus sesi SSH Anda.
Perilaku cold start dan restart
The data behind this chart
[
{
"label": "Flask, one route",
"cold_start_ms": 90
},
{
"label": "Flask + SQLAlchemy",
"cold_start_ms": 260
},
{
"label": "Django, admin disabled",
"cold_start_ms": 480
},
{
"label": "Django, admin enabled",
"cold_start_ms": 720
}
]Biaya boot harus dibayar dua kali: pada setiap deploy dan pada setiap restart otomatis setelah terjadi crash. Aplikasi Flask minimal siap dalam sekitar 90 ms, sedangkan Django dengan admin yang diaktifkan memerlukan sekitar 720 ms pada shared vCPU yang sama. Keduanya merupakan angka publik yang umum. Ukur aplikasi Anda sendiri karena dependensi memiliki pengaruh terbesar.
cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20Baris terakhir mencantumkan import paling lambat beserta waktu kumulatif dalam mikrodetik. Untuk Flask, jalankan flag yang sama terhadap modul Anda: python -X importtime -c "import app".
Dengan preload aktif, master menanggung biaya tersebut sekali dan setiap worker hasil fork dapat langsung start. Dengan preload nonaktif, setiap worker menanggung biaya tersebut, dan timeout milik gunicorn mencakup proses boot serta request. Worker yang belum mengirimkan tanda aktif dalam timeout detik akan dihentikan dan diganti. Akibatnya, aplikasi berat pada shared vCPU yang lambat dapat terus berada dalam loop restart dan tidak pernah melayani apa pun. Log menampilkan:
[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)Migrasi harus ditempatkan dalam unit, bukan dalam kode startup aplikasi. ExecStartPre berjalan sekali sebelum worker mana pun dibuat. Jika migrate ditempatkan di dalam aplikasi, tiga worker akan saling berlomba mendapatkan schema lock yang sama.
Bentuk deployment hampir sama
Pengelola proses
Kedua framework berjalan di bawah gunicorn, dan gunicorn berjalan di bawah systemd.
[Unit]
Description=gunicorn for site1
After=network.target
[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M
[Install]
WantedBy=multi-user.targetRuntimeDirectory=site1 membuat /run/site1 saat start dan menghapusnya saat stop, sehingga path socket selalu tersedia dengan owner yang benar. Baris umask = 0o007 dalam konfigurasi gunicorn membuat socket tersebut dapat ditulis oleh grup www-data. Dengan demikian, nginx dapat mengaksesnya.
Unit Flask menggunakan file yang sama dengan satu baris yang diubah: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, tanpa ExecStartPre. Argumen app:app terdiri atas module lalu callable. Jadi, error Failed to find attribute 'app' in 'app'. berarti module Anda tidak mendefinisikan variabel dengan nama tersebut. Pekerjaan terjadwal mengikuti pola yang sama. systemd timer menggantikan cron untuk perintah manajemen Django tanpa menambahkan task queue pada server dengan ukuran seperti ini.
File statis
Django dengan DEBUG = False sama sekali tidak menyajikan file statis. Tetapkan STATIC_ROOT, jalankan python manage.py collectstatic, lalu arahkan web server ke direktori output. Jika langkah ini dilewati, halaman admin dimuat tanpa styling dan log dipenuhi Not Found: /static/admin/css/base.css.
Ada dua cara yang wajar untuk menyajikannya. Blok nginx alias tidak membebani aplikasi Anda. WhiteNoise, yang ditambahkan sebagai middleware, menyajikan file dari worker dan menghilangkan kebutuhan akan blok nginx, tetapi menggunakan sedikit waktu worker untuk setiap file. Flask menyajikan folder static/ miliknya sendiri dalam development. Dalam production, arahkan proxy ke folder tersebut dengan alasan yang sama.
Reverse proxy
server {
listen 80;
server_name example.com;
location /static/ {
alias /srv/site1/static/;
expires 30d;
}
location / {
proxy_pass http://unix:/run/site1/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Di balik proxy apa pun, Django perlu diberi tahu bahwa request asli menggunakan HTTPS. Jika tidak, pemeriksaan cross site request forgery (CSRF) akan menolak form milik Anda sendiri.
ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")Jika server sudah menjalankan container, Traefik di depan beberapa aplikasi Docker Compose melakukan pekerjaan yang sama menggunakan label pada container, bukan satu file untuk setiap site.
Database yang dipilih
SQLite benar-benar memadai untuk satu application server dengan laju write beberapa kali per detik. SQLite juga menghilangkan satu daemon dari anggaran memori. Aktifkan write ahead logging (WAL) dan tetapkan busy timeout pada driver.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"init_command": "PRAGMA journal_mode=WAL;",
},
}
}Tanpa kedua opsi tersebut, Anda akan menemui django.db.utils.OperationalError: database is locked saat dua worker melakukan write secara bersamaan untuk pertama kalinya. Mode journal default memblokir pembacaan selama proses write, sedangkan timeout default hampir langsung menyerah. Penjelasan yang lebih panjang, termasuk kapan SQLite tidak lagi menjadi pilihan yang tepat, tersedia di menjalankan SQLite di production pada VPS.
PostgreSQL pada server 1 GB yang sama menggunakan 120 MB dari anggaran di atas, ditambah satu proses backend untuk setiap koneksi persisten. CONN_MAX_AGE milik Django mempertahankan satu koneksi terbuka untuk setiap worker, sehingga empat worker berarti empat backend. Biasanya, ini merupakan kompromi yang baik. Namun, hitung kebutuhan tersebut sebelum menetapkan jumlah worker. Jika Anda lebih memilih menjalankan database dalam container di samping aplikasi, menjalankan Docker pada VPS menawarkan kompromi yang sama dengan batas awal yang lebih tinggi. Daemon dan setiap container menambah overhead yang penting pada ukuran server seperti ini.
Manfaat yang diberikan dan biayanya
Megabita tambahan Django berisi berbagai komponen yang sudah tersedia dan dapat langsung bekerja sama: ORM dengan migrasi, sistem sesi dan autentikasi, model izin, lapisan formulir dengan perlindungan CSRF, mesin templat, perintah manajemen, dan admin. Admin adalah komponen yang sering diremehkan. Komponen ini merupakan editor database yang dapat langsung digunakan untuk model Anda, lengkap dengan pencarian dan filter, hanya dengan satu baris di INSTALLED_APPS.
Flask adalah kebalikannya. Anda mendapatkan routing, objek request, templat Jinja2, dan objek konfigurasi. Komponen lainnya harus Anda pilih sendiri. Ini menjadi manfaat nyata jika aplikasinya kecil karena tidak ada ORM yang dimuat jika Anda tidak pernah mengimpornya.
Masalah muncul pada kondisi di tengah. Jika Anda menambahkan SQLAlchemy untuk model, Alembic untuk migrasi, Flask-Login untuk sesi, Flask-WTF untuk formulir dan CSRF, serta ekstensi admin untuk back office, Anda telah merakit sesuatu dengan penggunaan memori seperti Django, tetapi tanpa koherensi Django. Setiap komponen memiliki siklus rilis dan pendekatan sendiri untuk menghubungkan komponen dalam aplikasi. Pada titik ini, Django menjadi pilihan yang lebih hemat, baik dalam penggunaan RAM maupun waktu yang Anda habiskan untuk melakukan upgrade.
Django vs Flask: aturan pengambilan keputusan
Gunakan Django jika aplikasi memiliki akun, konten yang dapat diedit, skema yang akan terus berubah, dan back office yang benar-benar akan digunakan. Gunakan Flask jika aplikasi merupakan antarmuka JSON di atas datastore yang sudah ada, atau penerima webhook tanpa HTML.
Penentu akhirnya adalah daftar tertulis. Catat setiap paket yang akan Anda instal di Flask untuk memperoleh rangkaian fitur yang diperlukan. Jika daftar tersebut mencakup ORM dan alat migrasi, berarti Anda sebenarnya sudah memilih Django dan hanya membayar biaya tambahan untuk mencapainya secara lebih lambat.
Ada satu kondisi yang benar-benar mengunggulkan Flask pada perangkat keras kecil: beberapa service kecil dalam satu host. Setiap service Flask merupakan proses murah yang berjalan di bawah unitnya sendiri. Menjalankan tiga situs Django pada satu VPS 1 GB berarti ada tiga salinan framework yang berada di memori secara bersamaan, sehingga perhitungan di atas tidak lagi berlaku. Jika hasil perhitungan terus menunjukkan kekurangan, memilih paket yang lebih besar sering kali merupakan solusi yang tepat, dan biaya VPS sebenarnya per bulan lebih mudah dibahas daripada menulis ulang aplikasi yang sudah berfungsi.
Mode kegagalan dan string yang akan Anda lihat
Worker menghilang lalu muncul kembali. Gunicorn mencetak [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Pesan ini muncul saat Gunicorn menghentikan worker yang tidak mengirim heartbeat dalam batas waktu maupun saat kernel menghentikan proses tersebut. Bedakan kedua kondisi ini dengan dmesg -T | grep -i "killed process". Baris di sana menunjukkan masalah memori, sehingga kurangi jumlah worker atau tambahkan swap.
Setiap halaman mengembalikan 400 dan log menampilkan Invalid HTTP_HOST header. Pesan lengkapnya adalah Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS. Django menolak request sebelum mencapai kode Anda karena ALLOWED_HOSTS kosong atau tidak menyertakan nama yang diteruskan proxy melalui Host.
Formulir gagal dengan Origin checking failed. Halaman menampilkan pesan bahwa verifikasi CSRF gagal. Kondisi ini terjadi di belakang proxy yang melakukan terminasi TLS: aplikasi melihat HTTP biasa, membentuk origin http://, lalu membandingkannya dengan request yang tiba melalui https://. Atur SECURE_PROXY_SSL_HEADER dan CSRF_TRUSTED_ORIGINS, lalu pastikan proxy benar-benar mengirim X-Forwarded-Proto.
nginx segera mengembalikan 502. Error log mencantumkan penyebabnya: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) berarti unit tidak sedang berjalan, sedangkan (13: Permission denied) berarti socket ada tetapi nginx tidak dapat membukanya. Penyebabnya adalah pengaturan umask dan group.
Halaman admin tidak memiliki styling. collectstatic belum dijalankan, atau path alias tidak cocok dengan STATIC_ROOT. Access log menampilkan 404 pada /static/admin/.
Operasi tulis gagal saat beban rendah. database is locked dari SQLite berarti WAL dinonaktifkan atau batas waktu busy terlalu singkat untuk dua worker yang menulis pada saat yang sama.
FAQ
Apakah Django terlalu berat untuk VPS 1 GB?
Tidak. Django dengan sedikit worker, nginx di depan, dan SQLite di belakang dapat berjalan dengan baik pada 1 GB. Kondisi mulai ketat ketika Anda menambahkan PostgreSQL dengan pengaturan default, cache, background worker, dan Docker pada server yang sama. Ukur proportional set size satu worker, kalikan dua untuk mengantisipasi lonjakan permintaan, lalu bandingkan totalnya dengan memori yang tersisa setelah sistem operasi dan database.
Berapa banyak worker gunicorn yang sebaiknya dijalankan pada satu vCPU?
Mulai dengan tiga worker, lalu lakukan pengukuran. Pada VPS kecil, memori biasanya menjadi batas utama. Karena itu, bagi RAM yang tersisa setelah sistem operasi dan database dengan dua kali proportional set size satu worker. Jika view Anda sebagian besar menunggu database atau API upstream, gunakan worker class gthread dengan sedikit worker dan beberapa thread pada setiap worker. Thread berbagi satu salinan framework yang telah dimuat dan menggunakan jauh lebih sedikit memori daripada proses tambahan.
Apakah saya memerlukan PostgreSQL, atau SQLite sudah cukup?
SQLite cukup untuk satu server aplikasi dengan laju penulisan sedang, dan SQLite menghilangkan satu daemon penuh dari anggaran memori. Aktifkan write ahead logging dan atur busy timeout. Jika tidak, penulisan bersamaan akan gagal dengan database is locked. Beralihlah ke PostgreSQL ketika lebih dari satu mesin harus melakukan penulisan, atau ketika Anda memerlukan fitur yang tidak disediakan SQLite, seperti penulisan berat secara bersamaan atau kontrol akses per role.
Apakah sebaiknya saya menjalankan uvicorn, bukan gunicorn?
Hanya jika Anda memiliki async view dan ada operasi nyata yang harus ditunggu. Flask adalah aplikasi WSGI. Karena itu, async view berjalan dalam event loop baru di dalam thread worker dan selesai sebelum permintaan berikutnya dimulai. Pengaturan ini tidak menambah konkurensi. Django async view memerlukan server ASGI agar dapat memberikan manfaat. Rilis uvicorn terbaru memindahkan worker class gunicorn ke package terpisah. Karena itu, baca dokumentasi uvicorn saat ini, bukan menyalin flag worker class lama dari tutorial yang lebih lama.
Mengapa worker saya hilang tanpa traceback?
Proses yang dihentikan oleh kernel karena out of memory killer menerima SIGKILL dan tidak dapat mencatat apa pun sebelum berhenti. Akibatnya, log aplikasi Anda hanya berhenti. Gunicorn mendeteksi jeda tersebut dan mencetak Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Konfirmasikan dengan dmesg -T | grep -i "killed process". Solusinya adalah mengurangi jumlah worker, atau menggunakan swapfile agar lonjakan penggunaan memori menyebabkan permintaan berjalan lambat, bukan proses mati.