API gateways are usually heavyweight. You spin up Kong or an AWS ALB with Lambda authorizers, configure a dozen YAML files, and end up with a system that costs more than the API it protects. For side projects and small-scale services, this is overkill.
PandaStack's edge functions let you run Python code in response to HTTP requests, with runtimes measured in milliseconds and no container to keep warm. This is a good fit for lightweight API gateway logic — validating JWT tokens, enforcing rate limits, rewriting requests, or proxying to a backend with extra headers injected.
What an edge function can and cannot do
Edge functions run in a sandboxed environment with a short timeout. They are not a replacement for your main API, but they can sit in front of it and handle cross-cutting concerns.
Good use cases:
- Check an
Authorizationheader and return 401 if missing or invalid - Rate-limit by IP address or API key using PandaStack's KV store
- Rewrite URLs (like
/v2/users/:id→/users?id=:id&version=2) - Inject custom headers before proxying to a backend
- Serve cached responses for idempotent GET requests
Bad use cases:
- Long-running tasks (timeouts are enforced; use a container app or cronjob instead)
- Heavy computation (CPU time is limited)
- Stateful workflows (functions are stateless; state must live in the KV store or a database)
PandaStack supports two runtimes for edge functions: nodejs and python. There is no Go or Rust runtime, so if you need raw speed for CPU-bound tasks, use a containerized service instead.
Write a Python function that validates a JWT
Create a file auth_gateway.py:
import json
import jwt
import os
from datetime import datetime
SECRET_KEY = os.environ.get('JWT_SECRET')
def handler(request):
"""
Validate a JWT in the Authorization header.
Return 401 if missing or invalid; otherwise proxy to the backend.
"""
auth_header = request.get('headers', {}).get('authorization', '')
if not auth_header.startswith('Bearer '):
return {
'statusCode': 401,
'body': json.dumps({'error': 'Missing or malformed Authorization header'}),
'headers': {'Content-Type': 'application/json'}
}
token = auth_header[7:] # Strip "Bearer "
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return {
'statusCode': 401,
'body': json.dumps({'error': 'Token expired'}),
'headers': {'Content-Type': 'application/json'}
}
except jwt.InvalidTokenError:
return {
'statusCode': 401,
'body': json.dumps({'error': 'Invalid token'}),
'headers': {'Content-Type': 'application/json'}
}
# Token is valid; proxy the request to the backend or return success
return {
'statusCode': 200,
'body': json.dumps({'message': 'Authenticated', 'user': payload['sub']}),
'headers': {'Content-Type': 'application/json'}
}The function reads the Authorization header, decodes the JWT using a secret stored in an environment variable, and returns 200 if valid or 401 if not. In a real gateway, the 200 case would forward the request to your backend API instead of returning a stub message.
Add a requirements.txt:
PyJWT==2.8.0Deploy the function via the CLI
panda functions create \
--name auth-gateway \
--runtime python \
--entry auth_gateway.py \
--handler handler \
--env JWT_SECRET=your-secret-keyPandaStack packages the function and its dependencies into a deployable unit and assigns it an invoke URL:
https://functions.pandastack.io/invoke/auth-gateway-xyz789Send a request with a JWT:
curl -X POST https://functions.pandastack.io/invoke/auth-gateway-xyz789 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."If the token is valid, you get a 200 response with the decoded payload. If expired or malformed, you get 401.
The function cold-starts on the first request after a period of inactivity. Subsequent requests while the function is warm return in under 50 milliseconds. Because there is no container to scale down, there is no KEDA warm-up delay or health check overhead.
Use the KV store for rate limiting
PandaStack's managed Redis (the KV store) is a good fit for per-user or per-IP rate limiting. When you link a KV instance to a function, PandaStack injects KV_URL, KV_REST_API_URL, and KV_REST_API_TOKEN as environment variables.
Update the function to count requests per IP:
import json
import os
import requests
KV_REST_URL = os.environ.get('KV_REST_API_URL')
KV_TOKEN = os.environ.get('KV_REST_API_TOKEN')
def handler(request):
ip = request.get('headers', {}).get('x-forwarded-for', '0.0.0.0').split(',')[0]
key = f'ratelimit:{ip}'
# Increment the request count for this IP
response = requests.post(
f'{KV_REST_URL}/incr/{key}',
headers={'Authorization': f'Bearer {KV_TOKEN}'}
)
count = response.json().get('result', 0)
# Set expiry on first request (60 seconds)
if count == 1:
requests.post(
f'{KV_REST_URL}/expire/{key}/60',
headers={'Authorization': f'Bearer {KV_TOKEN}'}
)
# Enforce a limit of 10 requests per minute
if count > 10:
return {
'statusCode': 429,
'body': json.dumps({'error': 'Rate limit exceeded'}),
'headers': {'Content-Type': 'application/json'}
}
return {
'statusCode': 200,
'body': json.dumps({'message': 'Request allowed', 'remaining': 10 - count}),
'headers': {'Content-Type': 'application/json'}
}This uses the Upstash-style REST API for Redis, which PandaStack exposes because native Redis clients require TLS SNI, and many clients do not support it. The REST façade works from any HTTP client.
Provision a KV store in the dashboard, link it to the function, and redeploy. Now the function enforces 10 requests per IP per minute, with the count stored in Redis and expired automatically after 60 seconds.
Proxy requests to a backend API
For a true gateway, the function should forward valid requests to your backend. Here is a pattern:
import json
import os
import requests
BACKEND_URL = os.environ.get('BACKEND_API_URL', 'https://api.yourapp.com')
def handler(request):
# Auth check here (same JWT logic as before)
# Extract the path and method from the incoming request
path = request.get('path', '/')
method = request.get('method', 'GET')
headers = request.get('headers', {})
body = request.get('body', '')
# Forward to the backend
backend_response = requests.request(
method,
f'{BACKEND_URL}{path}',
headers=headers,
data=body,
timeout=5
)
return {
'statusCode': backend_response.status_code,
'body': backend_response.text,
'headers': dict(backend_response.headers)
}Deploy this function and point your frontend at the function's invoke URL instead of the backend directly. The function becomes a thin auth and rate-limiting layer, and the backend never sees unauthenticated traffic.
This pattern keeps the gateway logic small (a few dozen lines of Python) and avoids the operational cost of a full API gateway service.
When to use this vs. a container app
Edge functions are billed per invocation (currently no charge on PandaStack; this may change as the feature matures). Container apps are billed per hour of runtime. For low-traffic APIs (under 1,000 requests per day), a function is cheaper and simpler. For high-traffic APIs, a long-running container avoids cold starts and is more predictable.
Functions scale automatically. You do not set replica counts or autoscaling policies. PandaStack spins up instances on demand and tears them down when traffic drops. This makes them a good fit for spiky workloads (webhooks, scheduled tasks triggered by cron) and a poor fit for sustained traffic (a WebSocket server, a long-polling endpoint).
If your gateway logic grows beyond a single file or needs a relational database connection pool, move it to a container app. Edge functions are for lightweight, stateless tasks that finish in under a second.
Deploy from CI with the API
Script function deploys in CI by calling the API directly. Zip the function and its dependencies:
zip -r function.zip auth_gateway.py requirements.txtUpload it:
curl -X POST https://api.pandastack.io/v1/functions \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-F "name=auth-gateway" \
-F "runtime=python" \
-F "handler=auth_gateway.handler" \
-F "code=@function.zip" \
-F "env[JWT_SECRET]=$JWT_SECRET"The API returns the function ID and invoke URL. Store the ID in your CI environment and use it for updates:
curl -X PUT https://api.pandastack.io/v1/functions/$FUNCTION_ID \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-F "code=@function.zip"This workflow keeps the function code version-controlled and deployed via the same pipeline as your main app.
Debugging a failed invocation
Function logs are visible in the dashboard under Functions → Logs. If an invocation fails, you will see the stack trace and the request payload that triggered it.
Common failures:
- Timeout: The function took too long. Reduce the work it does or move the logic to a container app.
- Import error: A dependency is missing from
requirements.txt. Add it and redeploy. - KV connection failed: The KV store is not linked to the function. Link it in the dashboard or via the CLI and redeploy.
Functions do not have SSH access or a persistent filesystem. Debugging is done through logs and local testing. Run the function locally by mocking the request payload:
if __name__ == '__main__':
test_request = {
'headers': {'authorization': 'Bearer test-token'},
'path': '/users/123',
'method': 'GET'
}
print(handler(test_request))This lets you iterate quickly without redeploying on every change.
Why Python instead of Node.js for this use case
PandaStack supports both nodejs and python runtimes for edge functions. Python is slower to cold-start than Node.js, but its standard library is richer for tasks like JWT parsing, hashing, and HTTP requests. If you already have backend logic in Python (a FastAPI service, a Django app), reusing that code in a function is easier than rewriting it in JavaScript.
For pure request routing or header manipulation, Node.js is faster. For crypto, auth, and external API calls, Python's ecosystem is more mature.
Choose based on what your team already knows and what libraries you need.
References
- [PandaStack edge functions documentation](https://docs.pandastack.io)
- [PyJWT library](https://pyjwt.readthedocs.io)
- [Redis REST API reference](https://upstash.com/docs/redis/features/restapi)