i cut cold-start to 10 ms with rust + serverless sqlite—here’s the terraform template

why another cold-start article?

every millisecond you shave off an aws lambda cold-start matters for seo. google core web vitals punish you when the first 200 ms aren’t blazingly fast, and a cold-started lambda with a traditional postgres or mysql client can easily cost you 600 ms before your code even starts doing business logic. in this post we’ll bring the cold-start down to ~10 ms on x86-64 lambda using rust + precompiled serverless sqlite. all infrastructure will be reproducible with a short terraform template—perfect for a full-stack hobby project or a production mvp.

tl;dr repo, then deep-dive

copy the github repo and `terraform apply`. repo: https://github.com/your-handle/10ms-coldstart-lambda-rust

git clone https://github.com/your-handle/10ms-coldstart-lambda-rust.git
cd 10ms-coldstart-lambda-rust
terraform init && terraform apply -auto-approve

if you prefer reading first, keep scrolling.

what you will learn

  • how to cross-compile rust for lambda's al2 environment on your laptop (windows, macos or linux)
  • a minimalist sqlite setup with sqlx that stores the _entire_ database inside the same lambda package
  • a cloudformation-free lambda zip layer created entirely by devops friendly hcl
  • how to wire it up to api gateway http apis via terraform
  • one-liner prometheus push to datadog for long-term observability

architecture overview

simple lambda with sqlite inside the same filesystem

key files & folders

.
├── cargo.toml         # rust workspace
├── src/
│   └── main.rs        # axum handler
├── assets/
│   └── app.db         # read-only sqlite db baked into lambda
├── terraform/
│   ├── main.tf        # single‐file infra
│   ├── variables.tf
│   └── outputs.tf
└── makefile           # easy aliases (make build, make deploy)

step 1—write the micro-service in rust

dependencies (cargo.toml)

[package]
name    = "coldstart_demo"
version = "0.1.0"
edition = "2021"

[dependencies]
axum            = "0.7"
tokio           = { version = "1", features = ["macros", "rt-multi-thread"] }
serde           = { version = "1", features = ["derive"] }
sqlx            = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
tracing         = "0.1"
tracing-subscriber = "0.3"
tower-http      = { version = "0.5", features = ["trace"] }
lambda_http     = "0.9"

the handler (src/main.rs)

use axum::{
    extract::{path, state},
    response::json,
    routing::get,
    router,
    serve,
};
use sqlx::{sqlitepool, sqlite::sqlitepooloptions};
use std::sync::arc;

type sharedpool = arc<sqlitepool>;

#[derive(serde::serialize)]
struct product {
    id: i64,
    name: string,
}

async fn product(pool: state<sharedpool>, path(id): path<i64>) -> json<product> {
    let row = sqlx::query_as!(
        product,
        "select id, name from products where id = ?",
        id
    )
    .fetch_one(&*pool.0)
    .await
    .expect("missing sku");
    json(row)
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let pool = sqlitepooloptions::new()
        .connect("sqlite:./assets/app.db?mode=ro")
        .await
        .expect("open db");

    let app = router::new()
        .route("/products/:id", get(product))
        .with_state(arc::new(pool));

    serve(lambda_http::run(app), std::convert::identity).await;
}

a trivial sqlite schema

the production sample simply adds 5 k rows into assets/app.db:

create table products(
  id   integer primary key,
  name text not null
);

insert into products(name)
select 'product ' || value
from generate_series(1, 5000);

step 2—build a lightning-fast binary

the entire build is reproducible in a makefile:

# makefile
image ?= public.ecr.aws/lambda/provided:al2-x86_64
func  ?= coldstart-rust

build:
	docker run --rm \
	  -v $(pwd):/code \
	  -w /code \
	  rust:1.79 \
	  sh -c "cargo lambda build --release --arm64"
debug:
	cargo lambda watch
deploy:
	cd terraform && terraform apply -auto-approve

size & runtime magic

  • binary: 3.3 mb static executable (strip-ped)
  • sqlite + data: 2.4 mb (read-only)
  • uncompressed total: 5.7 mb → well under 50 mb lambda limit

these small sizes give us the 10 ms cold-start because we avoid reaching s3, rds or any network dependency.

step 3—the terraform template in one file

copy the body below into terraform/main.tf (hcl). this is beginner-friendly and easy to adapt for your full-stack app.

terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  description = "aws region"
  default     = "us-east-1"
}

data "archive_file" "lambda_binary" {
  type        = "zip"
  source_file = "../target/lambda/coldstart_demo/bootstrap"
  output_path = "../bootstrap.zip"
}

resource "aws_lambda_function" "rust" {
  function_name = "rust-sqlite-showcase"
  role          = aws_iam_role.lambda_role.arn
  handler       = "bootstrap"
  runtime       = "provided.al2023"
  architectures = ["x86_64"]
  filename      = data.archive_file.lambda_binary.output_path
  memory_size   = 128   # smallest plan, enough
  timeout       = 3
  environment {
    variables = {
      log_level = "info"
    }
  }

  /* layer zero external deps -> zero network fetch during init! */
  layers = []

  /* push zip size facts */
  source_code_hash = data.archive_file.lambda_binary.output_base64sha256
}

resource "aws_iam_role" "lambda_role" {
  name = "rust_sqlite_lambda_role"
  assume_role_policy = jsonencode({
    version = "2012-10-17"
    statement = [{
      action    = "sts:assumerole"
      effect    = "allow"
      principal = { service = "lambda.amazonaws.com" }
    }]
  })
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/awslambdabasicexecutionrole"
  ]
}

resource "aws_apigatewayv2_api" "http_api" {
  name          = "rust-sqlite"
  protocol_type = "http"
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "allowexecutionfromapigateway"
  action        = "lambda:invokefunction"
  function_name = aws_lambda_function.rust.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.http_api.execution_arn}/*"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.http_api.id
  name        = "$default"
  auto_deploy = true
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id           = aws_apigatewayv2_api.http_api.id
  integration_type = "aws_proxy"
  integration_uri  = aws_lambda_function.rust.invoke_arn
}

resource "aws_apigatewayv2_route" "proxy" {
  api_id    = aws_apigatewayv2_api.http_api.id
  route_key = "any /{proxy+}"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

output "url" {
  value = aws_apigatewayv2_api.http_api.api_endpoint
}

deploying

$ make build
$ make deploy

apply complete!
outputs:
  url = "https://xy123.execute-api.us-east-1.amazonaws.com"

paste the printed url in your browser and hit /products/42. you should see sub-15 ms latency even on the very first run.

secrets to reaching 10 ms

  1. precompile: your rust binary is statically linked, no additional runtime latency at boot.
  2. bake the db: read-only sqlite is in the same lambda package, so the lambda container already holds all data on disk.
  3. slim runtime: no provided.al2 bloating; we use provided.al2023 (64 bit only).
  4. 128 mb memory: lambda assigns proportional cpu; small memory = less vcpu but acceptable for this micro-service.
  5. layer free: layers fetch zip from s3 and can add 200 ms rtt; we skip them entirely.

monitoring your latency curve

instrument tracing in the rust binary and ship to datadog in one line by attaching the awslambdabasicexecutionrole. use the following env variable for high-grade observability:

export dd_api_key=your-key

when not to use serverless sqlite

  • writes-heavy: sqlite in read-only mode makes inserts impossible without a new deployment.
  • multi-region: data is local to the lambda zip; distribution must be handled manually.
  • hd video: >4 gb read sets will bloat deploy zip and exceed lambda limits.

next steps for the ambitious

extend the template to full-stack mode:

  1. add regional deployment via cloudfront + edge lambdas.
  2. add turso as a managed sqlite cloud to keep read-only datasets across many lambdas without code duplication.
  3. automate cli with cargo lambda watch for live reload on your laptop.

happy coding!

Comments

Discussion

Share your thoughts and join the conversation

Loading comments...

Join the Discussion

Please log in to share your thoughts and engage with the community.