Minimal serverless JSON endpoints using Bash on AWS Lambda (
provided.al2023) with a Go bootstrap layer.
This is not just a Bash Lambda template.
This boilerplate embeds a compiled Go bootstrap (packaged as a Lambda layer) that communicates with the Lambda Runtime API using raw TCP, and delegates execution to shell handlers.
This project builds on the research documented in the Cloudless article:
Lambda Performance Deep Dive: Container Images, Raw TCP, and the UPX Trap
That article explains the runtime design decisions and benchmark results in detail.
That research concluded:
- The fastest and most predictable approach for shell-based Lambdas
- Is a compiled Go bootstrap
- Using raw TCP to communicate with the Lambda Runtime API
- Packaged as a layer
- Combined with
provided.al2023
This repository operationalizes that conclusion.
Sometimes you don’t need:
- A framework
- A container image
- Node or Python runtimes
- A database
- A data pipeline
Sometimes you just need:
A small, deterministic JSON endpoint that shapes data and returns it.
This repository provides:
- Shell-based Lambda functions
- Compiled Go bootstrap (raw TCP runtime client)
- Optional
jqlayer for data shaping - Terraform infrastructure (function + layer + function URL)
- Minimal deployment surface
- Extremely small function packages (often <1KB)
- Runtime:
provided.al2023 - Architecture:
arm64 - Custom Go bootstrap (compiled binary)
- Shell handler (
handler.sh) - Optional
jqlayer - Lambda Function URL (no API Gateway required)
Lambda Runtime API
↓
Go bootstrap (raw TCP client)
↓
Shell handler.sh
↓
curl / jq / system tools
↓
JSON response
The bootstrap handles:
- Fetching invocation events
- Passing payload to the shell handler
- Writing responses back to the Runtime API
- Minimal overhead, no SDKs
The shell handles:
- Calling upstream APIs
- Aggregating or normalizing data
- Returning JSON
- The standard Lambda runtime client adds avoidable overhead.
- SDK-based implementations increase cold start size and complexity.
- Container images introduce unnecessary weight.
- UPX compression can degrade real performance.
A small compiled Go binary speaking directly over raw TCP:
- Minimizes runtime overhead
- Avoids heavy dependencies
- Keeps cold starts predictable
- Keeps the system inspectable
For benchmark details and performance comparisons, refer to:
Lambda Performance Deep Dive: Container Images, Raw TCP, and the UPX Trap
This repository is the production-ready evolution of that research.
├── activate
├── app
│ └── src
│ └── handler.sh
├── infra
│ ├── main.tf
│ ├── output.tf
│ ├── terraform.tf
│ ├── variables.tf
│ └── versions.tf
├── layers
│ └── jq
│ ├── build.sh
│ ├── Dockerfile
│ ├── layer
│ └── README.md
├── README.md
├── runtime
│ ├── build
│ │ └── bootstrap
│ ├── build.sh
│ ├── go.mod
│ └── main.go
└── tf
Infrastructure leverages existing QL4B Terraform modules:
terraform-aws-lambda-functionterraform-aws-lambda-layer
This keeps the Terraform surface minimal.
If your endpoint uses jq for shaping or aggregation, you must build a Lambda-compatible layer for the target architecture.
Lambda supports:
- arm64
- x86_64
Because binaries must match the Lambda architecture, the jq layer is built per architecture.
The layers/jq directory contains:
- Dockerfile — multi-stage build producing a statically-linked binary
- build.sh — packages the layer in the correct /opt structure
- layer/ — final Lambda layer layout
To build the layer
cd layers/jq
./build.sh arm64 # or x86_64
The build process produces a Lambda-ready layer compatible with provided.al2023.
This layer is derived from the broader CLI layer collection maintained in:
https://github.com/ql4b/lambda-shell-layers
That repository contains additional optional tools (htmlq, yq, http-cli, uuid, etc.) built using the same pattern and compatible with this runtime model.
This template is ideal for:
REST API
↓
Shell + curl
↓
jq (aggregate / normalize)
↓
JSON
↓
Grafana / dashboards / other systems
Typical use cases:
- GitHub traffic aggregation
- Stripe summaries
- Multi-endpoint API fan-out
- Lightweight observability adapters
- Rapid data prototypes
- Internal tooling
No ingestion.
No persistence.
No ceremony.
git clone <repo>
cd lambda-shell-endpoint
cp .env.example .emv
Edit the .env and
source ./activate
cd runtime
./build.sh
cd ..
cd layers/jq
./build
tf init
tf apply
Terraform outputs a Lambda Function URL:
https://xxxx.lambda-url.<region>.on.aws/
curl https://xxxx.lambda-url.<region>.on.aws/ | jq
Done.
Inside handler.sh, define a function that:
- Reads the invocation payload
- Calls upstream APIs
- Shapes data with
jq - Returns a JSON document
Example:
run () {
curl -sS "https://api.example.com/data" \
| jq '{ result: .items }'
}
Keep the logic:
- Deterministic
- Stateless
- Inspectable
- Small
By default, Lambda Function URL may be public.
You should consider:
authorization_type = "AWS_IAM"- Adding a shared secret header check
- Restricting CORS origins
- Adding lightweight response caching
The template stays minimal.
Security posture is intentionally configurable.
Use this when:
- You need fast iteration
- You need a thin data-shaping layer
- You are building observability surfaces
- You want deterministic minimal infrastructure
Do not use this when:
- You need persistent state
- You need heavy compute
- You require complex authentication systems
- You are building a full application backend
This is aligned with the Cloudless approach:
- Infrastructure that gets out of your way
- Compiled where necessary, interpreted where useful
- Small, composable primitives
- Clear contracts
- Minimal moving parts
Shell as data engine.
Go as runtime spine.
Lambda as distribution layer.
JSON as contract.