Celery is Python’s most widely used distributed task queue — it handles everything from sending emails in the background to processing uploaded images to running scheduled reports. On a Hong Kong VPS, Celery workers running alongside your Django or FastAPI application process background tasks efficiently, keeping API responses fast by offloading heavy work to dedicated worker processes rather than blocking the web server during long operations.
When to Use Celery
- Send transactional emails without delaying API response
- Process uploaded images (resize, watermark, convert to WebP)
- Generate PDF reports or Excel exports
- Call slow external APIs (WeChat, payment gateways) asynchronously
- Scheduled maintenance tasks (cleanup, reports, data sync)
- Retry failed operations with exponential backoff
- Fan-out notifications (send to 10,000 users in parallel)
Step 1: Install Celery and Redis
<code">apt install -y redis-server systemctl enable --now redis-server source /opt/myapp/venv/bin/activate pip install celery[redis] redis flower
Step 2: Configure Celery
<code"># myapp/celery.py — Celery application configuration
import os
from celery import Celery
from celery.schedules import crontab
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
app = Celery('myapp')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# Scheduled tasks (Celery Beat)
app.conf.beat_schedule = {
'cleanup-old-sessions': {
'task': 'myapp.tasks.cleanup_expired_sessions',
'schedule': crontab(hour=2, minute=0),
'options': {'timezone': 'Asia/Hong_Kong'},
},
'sync-inventory': {
'task': 'myapp.tasks.sync_inventory',
'schedule': crontab(minute='*/15'),
},
'weekly-sales-report': {
'task': 'myapp.tasks.generate_weekly_report',
'schedule': crontab(hour=9, minute=0, day_of_week=1),
'options': {'timezone': 'Asia/Hong_Kong'},
},
}
app.conf.update(
broker_url=os.environ.get('REDIS_URL', 'redis://localhost:6379/0'),
result_backend=os.environ.get('REDIS_URL', 'redis://localhost:6379/0'),
task_serializer='json',
result_serializer='json',
accept_content=['json'],
task_acks_late=True,
worker_prefetch_multiplier=1,
task_reject_on_worker_lost=True,
task_max_retries=3,
task_default_retry_delay=60,
result_expires=3600,
timezone='Asia/Hong_Kong',
enable_utc=True,
)Django settings
<code"># settings.py
CELERY_BROKER_URL = env('REDIS_URL')
CELERY_RESULT_BACKEND = env('REDIS_URL')
# myapp/__init__.py
from .celery import app as celery_app
__all__ = ('celery_app',)Step 3: Define Tasks
<code"># myapp/tasks.py
from celery import shared_task
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=60, queue='email')
def send_order_confirmation(self, order_id: int, customer_email: str):
"""Send order confirmation email — retries up to 3 times."""
try:
from .models import Order
from django.core.mail import send_mail
order = Order.objects.get(id=order_id)
send_mail(
subject=f'Order Confirmation #{order.order_number}',
message=f'Your order {order.order_number} has been confirmed.',
from_email='orders@yourdomain.com',
recipient_list=[customer_email],
)
logger.info(f'Confirmation sent for order {order_id}')
except Exception as exc:
logger.error(f'Failed: {exc}')
raise self.retry(exc=exc)
@shared_task(queue='media', time_limit=120, soft_time_limit=90)
def process_uploaded_image(image_path: str, user_id: int):
"""Resize and optimise uploaded image to WebP, upload to MinIO."""
from PIL import Image
import boto3, io
s3 = boto3.client('s3', endpoint_url='https://storage.yourdomain.com')
with Image.open(image_path) as img:
for size_name, dims in {'thumb': (150,150), 'medium': (600,600)}.items():
resized = img.copy()
resized.thumbnail(dims, Image.LANCZOS)
buf = io.BytesIO()
resized.save(buf, format='WEBP', quality=85)
buf.seek(0)
s3.upload_fileobj(buf, 'app-uploads', f'users/{user_id}/{size_name}.webp')
return {'status': 'processed'}
@shared_task
def cleanup_expired_sessions():
from django.contrib.sessions.backends.db import SessionStore
SessionStore.clear_expired()
logger.info('Expired sessions cleaned')Step 4: Enqueue Tasks from Views
<code"># Fire and forget — returns immediately, no blocking
def create_order(request):
order = Order.objects.create(...)
send_order_confirmation.delay(order.id, request.user.email)
return JsonResponse({'order_id': order.id})
# Delayed execution (run in 5 minutes)
send_order_confirmation.apply_async(
args=[order.id, email],
countdown=300
)
# Scheduled for specific time
send_order_confirmation.apply_async(
args=[order.id, email],
eta=datetime.utcnow() + timedelta(hours=1)
)
# Check task status
from celery.result import AsyncResult
r = AsyncResult(task_id)
print(r.status) # PENDING / SUCCESS / FAILURE
print(r.result) # Return valueStep 5: Systemd Services
<code">cat > /etc/systemd/system/celery-worker.service << 'EOF' [Unit] Description=Celery Worker After=network.target redis.service [Service] Type=forking User=myapp WorkingDirectory=/opt/myapp EnvironmentFile=/opt/myapp/.env ExecStart=/opt/myapp/venv/bin/celery \ -A myapp worker \ --loglevel=info \ --concurrency=4 \ --queues=default,email,media \ --pidfile=/tmp/celery-worker.pid \ --logfile=/var/log/myapp/celery-worker.log \ --detach ExecStop=/opt/myapp/venv/bin/celery -A myapp control shutdown PIDFile=/tmp/celery-worker.pid Restart=always [Install] WantedBy=multi-user.target EOF cat > /etc/systemd/system/celery-beat.service << 'EOF'
[Unit]
Description=Celery Beat Scheduler
After=network.target redis.service celery-worker.service
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/celery \
-A myapp beat \
--loglevel=info \
--scheduler django_celery_beat.schedulers:DatabaseScheduler
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now celery-worker celery-beatStep 6: Flower — Task Monitoring UI
<code">cat > /etc/systemd/system/celery-flower.service << 'EOF'
[Unit]
Description=Celery Flower Monitor
After=network.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/celery \
-A myapp flower \
--port=5555 \
--address=127.0.0.1 \
--basic_auth=admin:StrongFlowerPass!
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now celery-flowerFlower UI at https://yourdomain.com/flower/ shows active workers, task history, success/failure rates, and queue depths in real time.
Queue Strategy
| Queue | Workers | Tasks |
|---|---|---|
| default | 4 | General background tasks |
| 2 | Email sending | |
| media | 2 | Image/video processing |
| critical | 2 | Payments, urgent notifications |
Conclusion
Celery on a Hong Kong VPS with Redis transforms synchronous bottlenecks into fast async operations — emails, image processing, report generation, and external API calls all move off the critical request path. Redis running on the same VPS keeps task enqueue latency under 1ms, Celery Beat handles scheduled jobs, and Flower provides operational visibility. A 4 GB VPS runs Django/FastAPI, Celery workers, Redis, and PostgreSQL together for most production workloads.
Process jobs async: Browse Server.HK Hong Kong VPS plans — a 4 GB plan handles the full async stack alongside your web application.