Skip to main content

Serverless mock adapter

Some teams deploy a test mock as a short-lived function rather than as a long-running service. The serverless adapter exposes the portable mock runtime through the narrow function interfaces of the supported providers, so a mock bundle can be answered by a Lambda, a Cloud Run function, or an Azure Function — no container, no open port, no idle cost.

Runtimeapiome-mock ≥ 0.7.0 (the same wheel the CLI and the image use)
Providersaws-lambda · gcp-functions · azure-functions
Preflightapiome-mock serverless --provider aws-lambda --bundle BUNDLE
Conformanceapiome-mock serverless --provider aws-lambda --conformance
ConfigurationAPIOME_MOCK_* environment variables only (a function has no command line)

The adapter adds no mock semantics of its own. It builds the same ASGI application apiome-mock run serves and drives it in-process, so routing, validation, scenarios, chaos, stateful CRUD, fixture packs, /health, and /ready are literally the same code as the hosted and CLI runtimes. The whole conformance corpus is run through each provider's real event shape in CI, so that is tested rather than asserted.


Quick start​

Export a bundle, put it next to your function, and point the runtime at it.

AWS Lambda​

my-function/
├── mock-bundle.json
└── requirements.txt # apiome-mock==0.7.0
SettingValue
Handlerapiome_mock.serverless.aws_lambda_handler
APIOME_MOCK_BUNDLE/var/task/mock-bundle.json
RouteANY /{proxy+} (API Gateway) or a Lambda Function URL
curl https://abc123.execute-api.eu-west-1.amazonaws.com/acme-corp/petstore/1.0.0/pets

Payload formats 1.0 (REST API, ALB) and 2.0 (HTTP API, Function URLs) are both decoded, and the response is rendered in whichever format the event arrived in — one handler serves either wiring.

Google Cloud Run functions​

# main.py
import functions_framework
from apiome_mock.serverless import gcp_functions_handler

@functions_framework.http
def mock(request):
return gcp_functions_handler(request)
gcloud functions deploy petstore-mock \
--gen2 --runtime python312 --entry-point mock --trigger-http \
--set-env-vars APIOME_MOCK_BUNDLE=/workspace/mock-bundle.json

Azure Functions​

# function_app.py
import azure.functions as func
from apiome_mock.serverless import azure_functions_handler

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.route(route="{*path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
def mock(req: func.HttpRequest) -> func.HttpResponse:
return azure_functions_handler(req)

Anything with an ASGI shim​

apiome_mock.serverless.asgi_app() returns the runtime's ASGI application for the configured bundle, which skips event translation entirely:

import azure.functions as func
from apiome_mock.serverless import asgi_app

app = func.AsgiFunctionApp(app=asgi_app(), http_auth_level=func.AuthLevel.ANONYMOUS)

Configuration​

A function has no command line, so every knob comes from the environment variables declared by the portable runtime — the same names, the same defaults, documented in Portable mock runtime. No configuration file and no .env are read.

VariableMeaning in a function
APIOME_MOCK_BUNDLEPath to the bundle inside the deployment package. Required.
APIOME_MOCK_BUNDLE_SECRETHMAC secret the bundle signature must verify against. Set it from the provider's secret store; never bake it into the package.
APIOME_MOCK_REQUIRE_SIGNATURERefuse to initialize unless the bundle is signed.
APIOME_MOCK_BASE_PATHversion mirrors the hosted URL shape; root serves spec paths at /.
APIOME_MOCK_ACCESS_LOGfalse turns off the per-invocation serverless_invocation line.
APIOME_MOCK_SESSION_*Session caps — but read session state first.

APIOME_MOCK_HTTP_HOST and APIOME_MOCK_HTTP_PORT are ignored: nothing binds a socket.


Cold start​

The bundle is read, verified, and compiled once per execution environment, at import time, and reused by every warm invocation. What that costs is measured rather than guessed:

  • logged as serverless_cold_start, with the split between verifying the bundle and starting the app;
  • returned on every response as X-Apiome-Mock-Cold-Start (true on the invocation that paid for it) and X-Apiome-Mock-Cold-Start-Ms;
  • checked against the provider's published budget by apiome-mock serverless.
$ apiome-mock serverless --provider aws-lambda --bundle petstore-1.0.0-mock-bundle.json
Serverless preflight: AWS Lambda (aws-lambda)
handler apiome_mock.serverless.aws_lambda_handler
bundle petstore-1.0.0-mock-bundle.json
digest sha256:90591f2f… (signed)
size 126.4 KiB
cold start 41 ms
warm call 0.4 ms
limits https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html (read 2026-08-26)
[NOTE ] bundle-fits-package-budget: The bundle is 126.4 KiB, 0.0% of the 250.0 MiB package budget on AWS Lambda.
[NOTE ] cold-start-within-budget: Initialization took 41 ms …, 0.4% of the budget. AWS Lambda allows 10s for initialization.
AWS Lambda: ready (0 error(s), 0 warning(s))

Preflight actually performs the initialization it reports, so the number is real. --json emits the whole report; exit code 7 means the bundle cannot be deployed as it stands.

Every response also carries X-Apiome-Mock-Bundle-Digest. Assert it: a green suite proves nothing if nobody can tell which artifact answered it.


Published limits​

These are the providers' own published numbers, kept as data in apiome_mock/serverless_providers.py so the preflight and this table cannot drift apart. Verify them against the linked page before you rely on one — providers move them.

AWS LambdaGoogle Cloud Run functionsAzure Functions
Package (uncompressed)250 MiB500 MiB1 GiB
Request payload10 MiB32 MiB100 MiB
Response payload6 MiB32 MiB100 MiB
Function timeout900 s3600 s600 s
Front-door timeout29 s3600 s230 s
Metered init budget10 s— (charged to the first request)— (charged to the first request)
Limits pagedocsdocsdocs

Two of these bite in practice:

The front-door timeout, not the function timeout, is your deadline. An API Gateway integration gives up at 29 s however generous the Lambda's own 900 s is. A chaos delay has to stay inside it.

The package budget is shared. The runtime and its dependencies live in the same package as the bundle, so preflight warns once the bundle alone passes a quarter of the limit.

Payloads outside the limits are refused by the adapter as application/problem+json — 413 for a request, 502 for a response — rather than truncated by the provider into something that looks like a mock bug:

{
"type": "https://apiome.dev/problems/serverless-response-too-large",
"title": "Bad Gateway",
"status": 502,
"detail": "The mock response is 7340032 bytes; AWS Lambda returns at most 6291456 bytes from a function.",
"maxResponseBytes": 6291456
}

Session state​

X-Mock-Session state lives in the execution environment's memory. It survives warm invocations on one instance and is absent on a cold one, and a provider that scales out will route the next request to an instance that has never seen the session.

That is a property of function environments, not a defect — so plan around it:

  • Seed, don't accumulate. Reset a session to a fixture pack at the start of a test (POST …/__mock__/session/reset) instead of relying on a resource an earlier request created.
  • Stateless assertions are portable. Example-first responses, schema synthesis, scenarios, declarative rules, and chaos are all deterministic from the bundle alone and behave identically on every instance.
  • Need durable state? Run the container runtime instead. One process, one session store.

Concurrency is serialized: an instance answers one invocation at a time. Where a provider allows more than one concurrent request per instance (Cloud Run functions), raise the instance count rather than the concurrency.


No provider secret in the bundle​

A bundle already drops and re-checks credential-shaped content at export and at load. The adapter adds a provider-specific layer on top, before the bundle is compiled:

CodeWhat it catches
aws-access-key-idAKIA… / ASIA… access key ids
gcp-service-account-keya service-account key document, embedded as text or as structure
gcp-api-keyAIza… API keys
azure-shared-keystorage connection strings and shared access keys
private-key-blockPEM private key blocks

Fixtures travel base64-encoded, so they are re-scanned after decoding rather than trusted for having been unreadable. A bundle carrying any of these cannot be loaded at all, and preflight reports it with a JSON pointer to the offending field:

[ERROR ] provider-secret-aws-access-key-id: An AWS access key id (AKIA…/ASIA…) is embedded in the
bundle. Remove it from /spec/info/description and re-export the bundle.

The bundle signing secret is read from APIOME_MOCK_BUNDLE_SECRET and nowhere else — never from the event, and never from the bundle. Put it in the provider's secret store (Secrets Manager, Secret Manager, Key Vault) and reference it as an environment variable, so it is not in the deployment package a reader can download.


Proving an invocation is faithful​

--conformance runs the shared corpus through the provider's own event shape against the bundle packaged with the runtime, which proves what an in-process call cannot: that the translation is faithful, and therefore that a function invocation answers a bundle exactly as every other runtime does.

apiome-mock serverless --provider aws-lambda --conformance
Conformance through AWS Lambda events:
[PASS] scenario-sequences-advance-per-session
…
34/34 conformance cases passed

Exit codes match the rest of the portable CLI: 0 success, 2 configuration, 3 invalid bundle, 4 incompatible bundle, 5 conformance failure, 7 preflight failure.


Structured logs​

Every line is one JSON object, as in the served runtime.

eventWhenNotable fields
serverless_cold_startOnce per execution environmentruntime, mount, coldStart, bundle
serverless_invocationOne per invocationmethod, path, status, duration_ms, cold_start, remaining_ms, digest
serverless_response_too_largeA response exceeded the provider's payload limitprovider, bytes, limit, path

remaining_ms is the provider's own countdown when it reports one (AWS Lambda's get_remaining_time_in_millis). It is logged and never acted on: a mock that behaved differently near its deadline would not be deterministic.


What the serverless adapter does not do​

Everything the portable runtime does not do applies here too — plus:

  • Durable sessions. See session state.
  • Streaming responses. An invocation returns one buffered payload.
  • gRPC, SSE, and WebSocket. HTTP only, as in the portable runtime.
  • Fetching a bundle at initialization. The bundle ships inside the package. A network read at cold start is what turns a fast mock into a flaky one.