An Express API is overkill for a single webhook endpoint or a form submission handler. You're running a full Node.js process, managing dependencies, and paying for idle time even when no requests come in. Edge functions solve this by running your code only when a request arrives, then shutting down immediately.
PandaStack edge functions use the nodejs runtime (Python is also available, but this post covers Node.js). You provide a JavaScript handler, deploy it via the API or CLI, and get an invoke URL. Cold starts are under a second, and the function scales to zero when idle — no charge for downtime.
Write the function
Create a handler.js file:
module.exports = async (event) => {
const { method, path, headers, body } = event;
if (method === 'POST' && path === '/webhook') {
const payload = JSON.parse(body);
console.log('Webhook received:', payload);
// Process the webhook (e.g., save to database, trigger workflow)
return {
statusCode: 200,
body: JSON.stringify({ message: 'Webhook processed' })
};
}
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' })
};
};The function receives an event object with request metadata and returns a response object with statusCode and body. This is the AWS Lambda signature, which many serverless platforms use.
Deploy via the REST API
Upload the function as a multipart form:
curl -X POST https://api.pandastack.io/v1/functions \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-F "name=webhook-handler" \
-F "runtime=nodejs" \
-F "handler=@handler.js"PandaStack bundles the code, deploys it to the edge runtime, and returns an invoke URL:
{
"success": true,
"data": {
"functionId": "fn_abc123",
"invokeUrl": "https://functions.pandastack.io/fn_abc123/invoke"
}
}The function is now live. Send a POST request to test it:
curl -X POST https://functions.pandastack.io/fn_abc123/invoke/webhook \
-H "Content-Type: application/json" \
-d '{"event": "user.signup", "userId": 42}'Response:
{"message": "Webhook processed"}Deploy via the CLI
panda login
panda functions deploy \
--name webhook-handler \
--runtime nodejs \
--handler handler.jsThe CLI uploads the file and prints the invoke URL.
Add dependencies
If your function uses npm packages, create a package.json:
{
"dependencies": {
"stripe": "^14.0.0"
}
}Then zip the function and its dependencies:
npm install
zip -r function.zip handler.js package.json node_modules/Upload the zip:
curl -X POST https://api.pandastack.io/v1/functions \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-F "name=stripe-webhook" \
-F "runtime=nodejs" \
-F "handler=@function.zip"PandaStack extracts the zip, runs npm install if package.json is present, and bundles the result into the edge runtime.
Environment variables
Hardcoding secrets in the function code is a mistake. Pass them as environment variables:
curl -X POST https://api.pandastack.io/v1/functions \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-F "name=stripe-webhook" \
-F "runtime=nodejs" \
-F "handler=@function.zip" \
-F "env[STRIPE_SECRET_KEY]=sk_test_xyz"In the function, read it as process.env.STRIPE_SECRET_KEY:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
module.exports = async (event) => {
const payload = JSON.parse(event.body);
const signature = event.headers['stripe-signature'];
const eventObj = stripe.webhooks.constructEvent(
event.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
console.log('Stripe event:', eventObj.type);
return {
statusCode: 200,
body: JSON.stringify({ received: true })
};
};Invoke the function from your app
Your frontend or backend can call the function over HTTP:
const response = await fetch('https://functions.pandastack.io/fn_abc123/invoke/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'user.signup', userId: 42 })
});
const result = await response.json();
console.log(result);The function runs only when this request arrives. If no one calls it for an hour, it consumes zero resources.
Authenticate function invocations
By default, the invoke URL is public. Anyone with the URL can call the function. To restrict access, generate a function token:
curl -X POST https://api.pandastack.io/v1/functions/<function-id>/tokens \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-d '{"name": "frontend-token"}'The response includes a token. Pass it as a bearer token in requests:
curl -X POST https://functions.pandastack.io/fn_abc123/invoke/webhook \
-H "Authorization: Bearer ft_xyz" \
-H "Content-Type: application/json" \
-d '{"event": "user.signup"}'Without the token, the function returns 401 Unauthorized.
Cold starts and warm instances
The first request to a function after it's been idle takes about 500-800ms because the runtime initializes the Node.js environment. Subsequent requests (within a few minutes) are faster because the instance is warm.
If your function is latency-sensitive, keep it warm by pinging it every 5 minutes:
curl https://functions.pandastack.io/fn_abc123/invoke/healthThis is only necessary for very low-traffic functions. Anything with a request per minute or more will stay warm naturally.
Debugging failures
Function not found: The invoke URL is wrong, or the function was deleted. Check panda functions list.
Module not found: A dependency is missing. Make sure you zipped node_modules/ or included package.json.
Timeout: Functions have a 30-second execution limit. If your function takes longer, it gets killed. Offload long-running work to a cronjob or background worker.
502 Bad Gateway: The function threw an uncaught exception. Check the logs (Dashboard → Functions → Logs) for the stack trace.
Compare to a full API
A container-based Express API runs constantly and costs per hour. An edge function costs per invocation. If your endpoint gets 1000 requests per day, a function is cheaper. If it gets 10 requests per second, a container is more cost-effective.
Edge functions also scale automatically. A container needs Horizontal Pod Autoscaling (HPA) configuration to add replicas under load, but functions spawn instances instantly.
When to use edge functions
- Webhooks: GitHub, Stripe, Slack, Twilio
- Form submissions: Contact forms, newsletter signups
- API proxies: Transform requests to a third-party API
- Scheduled tasks: Combined with a cronjob that triggers the function
- Low-traffic APIs: Endpoints that get a few requests per hour
When NOT to use edge functions
- Stateful services: Functions are ephemeral; use a container with persistent storage
- Long-running tasks: Anything over 30 seconds needs a container or cronjob
- WebSocket servers: Functions handle HTTP requests, not persistent connections
- High-throughput APIs: Constant traffic makes a container cheaper
References
- [PandaStack edge functions](https://docs.pandastack.io/functions)
- [AWS Lambda event structure](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html)
- [Serverless best practices](https://docs.aws.amazon.com/lambda/latest/operatorguide/best-practices.html)