Best Practices & Troubleshooting
Best practices for using ngrok with Django-CFG and solutions to common issues.
Best Practices
1. Development Only
Ngrok should only be used in development, never in production.
# ✅ CORRECT - ngrok only in development
from django_cfg import DjangoConfig, NgrokConfig
class MyConfig(DjangoConfig):
ngrok: NgrokConfig = NgrokConfig(
enabled=True # Automatically disabled if DEBUG=False
)
# ❌ WRONG - enable ngrok in production
# Ngrok is NOT for production! Only for local development.
Even better - explicit environment check:
from .environment import env
class MyConfig(DjangoConfig):
# Ngrok only in development
ngrok: NgrokConfig = NgrokConfig(
enabled=(env.environment == "development")
)
2. Use Helper Functions
Always use Django-CFG helper functions instead of manual URL construction.
# ✅ CORRECT - automatic fallback
from django_cfg.modules.django_ngrok import get_webhook_url
webhook_url = get_webhook_url("/webhooks/")
# If ngrok active: "https://abc123.ngrok.io/webhooks/"
# If not: "http://localhost:8000/webhooks/" (or api_url from config)
# ❌ WRONG - hardcode URL
webhook_url = "http://localhost:8000/webhooks/" # Won't work with ngrok!
# ❌ WRONG - manual construction
import os
ngrok_url = os.environ.get('NGROK_URL')
webhook_url = f"{ngrok_url}/webhooks/" if ngrok_url else "http://localhost:8000/webhooks/"
3. Check Tunnel Status Before Critical Operations
# ✅ CORRECT - check before using
from django_cfg.modules.django_ngrok import is_tunnel_active, get_webhook_url
if is_tunnel_active():
print("✅ Using ngrok tunnel for webhooks")
else:
print("⚠️ Ngrok not active, using fallback URL")
webhook_url = get_webhook_url("/webhooks/")
# ❌ WRONG - assume tunnel is always active
webhook_url = get_webhook_url("/webhooks/") # May return fallback URL!
4. Log Webhook Events
Always log webhook events for debugging.
# ✅ CORRECT - log webhooks for debugging
import logging
logger = logging.getLogger(__name__)
def webhook_handler(request):
logger.info(f"Webhook received from {request.META.get('HTTP_HOST')}")
logger.debug(f"Headers: {dict(request.headers)}")
logger.debug(f"Payload: {request.body.decode()}")
# Process webhook
# ...
return JsonResponse({"status": "ok"})
5. Environment-Specific Configuration
Use different configurations for different environments.
# ✅ CORRECT - different settings for dev/prod
from .environment import env
class MyConfig(DjangoConfig):
# Ngrok only in development
ngrok: NgrokConfig = NgrokConfig(
enabled=(env.environment == "development"),
webhook_path="/api/webhooks/"
)
# Production uses real domain
api_url: str = (
"https://api.myapp.com"
if env.environment == "production"
else "http://localhost:8000"
)
6. Use Type Hints
Always use type hints for better IDE support.
# ✅ CORRECT - type hints
from typing import Optional
from django_cfg.modules.django_ngrok import get_tunnel_url
def get_public_url() -> Optional[str]:
"""Get public URL for webhooks."""
return get_tunnel_url()
# ✅ CORRECT - type hints for service
class WebhookService:
def __init__(self):
self.base_url: str = get_tunnel_url() or "http://localhost:8000"
def get_webhook_url(self, path: str) -> str:
return f"{self.base_url}{path}"
Common Issues
Issue 1: Tunnel Not Starting
Symptom:
$ python manage.py runserver_ngrok
❌ Error: No module named 'ngrok'
Solution:
# Check that ngrok is available
python -c "import ngrok; print('OK')"
# If error "No module named 'ngrok'":
pip install ngrok
# For Python < 3.12 may need:
pip install pyngrok
Verify installation:
# Check ngrok is installed
import ngrok
print(f"Ngrok version: {ngrok.__version__}")
Issue 2: Webhooks Not Received
Symptom:
External service sends webhook but Django doesn't receive it
Solution 1: Check tunnel is active
from django_cfg.modules.django_ngrok import is_tunnel_active, get_tunnel_url
print(f"Tunnel active: {is_tunnel_active()}")
print(f"Tunnel URL: {get_tunnel_url()}")
# If not active:
# - Restart with: python manage.py runserver_ngrok
# - Verify ngrok: NgrokConfig(enabled=True)
Solution 2: Check ALLOWED_HOSTS
from django.conf import settings
print(f"ALLOWED_HOSTS: {settings.ALLOWED_HOSTS}")
# Should include ngrok host like:
# ['localhost', '127.0.0.1', 'abc123.ngrok.io']
# If not:
# - Restart server with runserver_ngrok
# - Check NgrokConfig(enabled=True)
Solution 3: Verify webhook URL
from django_cfg.modules.django_ngrok import get_webhook_url
webhook_url = get_webhook_url('/api/webhooks/')
print(f"Webhook URL: {webhook_url}")
# Should be like: https://abc123.ngrok.io/api/webhooks/
# NOT: http://localhost:8000/api/webhooks/
Solution 4: Check URL routing
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# Ensure webhook path exists
path('api/webhooks/stripe/', views.stripe_webhook, name='stripe_webhook'),
]
# Test URL manually:
# curl https://abc123.ngrok.io/api/webhooks/stripe/
Issue 3: Auth Token Issues
Symptom:
Error: Invalid ngrok auth token
Solution:
# If need advanced features (custom domain, etc):
export NGROK_AUTHTOKEN="your-ngrok-token"
# Get token from: https://dashboard.ngrok.com/get-started/your-authtoken
Or configure in code:
from django_cfg import NgrokConfig, NgrokAuthConfig
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
auth=NgrokAuthConfig(
authtoken_from_env=True # Load from NGROK_AUTHTOKEN
)
)
Issue 4: CSRF Token Errors
Symptom:
403 Forbidden - CSRF verification failed
Solution:
# Disable CSRF for webhook endpoints
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def stripe_webhook(request):
# Process webhook
return JsonResponse({"status": "ok"})
Or add ngrok host to trusted origins:
# settings.py (generated by Django-CFG)
CSRF_TRUSTED_ORIGINS = [
"https://*.ngrok.io",
"https://*.ngrok-free.app",
]
Issue 5: Tunnel URL Changes
Symptom:
Ngrok URL changes on each restart (free plan)
Solutions:
Option 1: Use helper functions (recommended)
# ✅ Always use helper functions
from django_cfg.modules.django_ngrok import get_webhook_url
# URL automatically updated on each restart
webhook_url = get_webhook_url("/webhooks/")
Option 2: Custom domain (paid plan)
from django_cfg import NgrokConfig, NgrokTunnelConfig, NgrokAuthConfig
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
auth=NgrokAuthConfig(
authtoken_from_env=True # Requires token
),
tunnel=NgrokTunnelConfig(
domain="myapp.ngrok.io" # Fixed domain (paid plan)
)
)
Issue 6: Slow Webhook Response
Symptom:
Webhooks timeout or are very slow
Solutions:
Solution 1: Enable compression
from django_cfg import NgrokConfig, NgrokTunnelConfig
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
tunnel=NgrokTunnelConfig(
compression=True # Enable gzip compression
)
)
Solution 2: Use async webhook processing
# Process webhooks asynchronously
import django_rq
from django.http import JsonResponse
@csrf_exempt
def stripe_webhook(request):
# Immediately return success
# Process webhook in background
payload = request.body
process_stripe_webhook.delay(payload.decode())
return JsonResponse({"status": "received"})
@task
async def process_stripe_webhook(payload: str):
# Process webhook in background
# ...
pass
Issue 7: Environment Variables Not Set
Symptom:
print(os.environ.get('NGROK_URL')) # None
Solution:
# Make sure you use runserver_ngrok
python manage.py runserver_ngrok # NOT runserver!
# Verify variables are set:
echo $NGROK_URL
echo $NGROK_HOST
echo $NGROK_SCHEME
In Python:
import os
# Check environment variables
ngrok_url = os.environ.get('NGROK_URL')
if not ngrok_url:
print("⚠️ NGROK_URL not set!")
print("Did you use 'runserver_ngrok' instead of 'runserver'?")
Issue 8: Multiple Tunnels
Symptom:
Multiple ngrok tunnels running at same time
Solution:
# Stop all ngrok processes
pkill -f ngrok
# Restart server
python manage.py runserver_ngrok
Or programmatically:
from django_cfg.modules.django_ngrok import get_ngrok_service
# Stop existing tunnel
service = get_ngrok_service()
service.stop_tunnel()
# Start new tunnel
service.start_tunnel(port=8000)
Debugging Tips
1. Check Ngrok Web Interface
Ngrok provides a web interface for debugging:
# Start server
python manage.py runserver_ngrok
# Open ngrok web interface
# http://127.0.0.1:4040
# Shows:
# - All HTTP requests
# - Request/response details
# - Timing information
2. Enable Verbose Logging
# config.py
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Or in Django settings:
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django_cfg.modules.django_ngrok': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
3. Test Webhook Manually
# Test webhook with curl
curl -X POST https://abc123.ngrok.io/api/webhooks/test/ \
-H "Content-Type: application/json" \
-d '{"event": "test", "data": {"amount": 1000}}'
# Should return webhook response
4. Monitor Django Logs
# In webhook handler
import logging
logger = logging.getLogger(__name__)
def webhook_handler(request):
logger.info(f"📥 Webhook received")
logger.debug(f"Headers: {dict(request.headers)}")
logger.debug(f"Body: {request.body.decode()}")
# Process webhook
result = process_webhook(request)
logger.info(f"✅ Webhook processed: {result}")
return JsonResponse({"status": "ok"})
Performance Optimization
1. Background Processing
Process webhooks asynchronously:
import django_rq
from django.http import JsonResponse
@csrf_exempt
def webhook_handler(request):
# Immediately acknowledge receipt
payload = request.body.decode()
process_webhook_task.delay(payload)
return JsonResponse({"status": "received"})
@task
async def process_webhook_task(payload: str):
# Process in background
# ...
pass
2. Enable Compression
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
tunnel=NgrokTunnelConfig(
compression=True # Faster for large payloads
)
)
3. Use Webhook Queues
# Queue webhooks for processing
from django.core.cache import cache
@csrf_exempt
def webhook_handler(request):
# Add to queue
webhook_id = str(uuid.uuid4())
cache.set(f"webhook:{webhook_id}", request.body, timeout=3600)
# Process asynchronously
process_webhook_queue.send(webhook_id)
return JsonResponse({"status": "queued"})
Security Considerations
1. Verify Webhook Signatures
Always verify webhook signatures from external services:
# Stripe signature verification
import stripe
@csrf_exempt
def stripe_webhook(request):
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except stripe.error.SignatureVerificationError:
return JsonResponse({"error": "Invalid signature"}, status=400)
# Process verified event
# ...
2. Use HTTPS Only
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
tunnel=NgrokTunnelConfig(
bind_tls=True # Force HTTPS
)
)
3. Password Protect Tunnel (Testing)
ngrok: NgrokConfig = NgrokConfig(
enabled=True,
tunnel=NgrokTunnelConfig(
basic_auth=["admin:supersecret"] # Require authentication
)
)
Summary
Key Takeaways:
✅ Use runserver_ngrok instead of runserver
✅ Always use helper functions (get_webhook_url(), etc.)
✅ Check tunnel status before critical operations
✅ Log all webhook events for debugging
✅ Verify webhook signatures from external services
✅ Process webhooks asynchronously for better performance
✅ Use ngrok web interface (localhost:4040) for debugging
✅ Development only - never use in production
Next Steps
- Overview - Back to ngrok overview
- Configuration - Advanced configuration options
- Webhook Examples - More integration examples
See Also
- Django-RQ Integration - Background task processing
- Configuration Guide - DjangoConfig reference
- Deployment Guide - Production deployment