Lambda
Protocol: REST JSON
Endpoint: http://localhost:4566/2015-03-31/functions/...
Floci Lambda runs your function code locally inside real Docker containers - close enough as AWS Lambda does (using Firecracker micro VM).
Supported Operations
| Operation | Description |
|---|---|
CreateFunction |
Deploy a Lambda function |
GetFunction |
Get function details and download URL |
GetFunctionConfiguration |
Get runtime configuration |
ListFunctions |
List all functions |
UpdateFunctionCode |
Upload new code |
UpdateFunctionConfiguration |
Update runtime, handler, memory, timeout, environment, architectures, tracing, layers, and more |
DeleteFunction |
Remove a function |
Invoke |
Invoke a function synchronously or asynchronously |
CreateEventSourceMapping |
Connect SQS / Kinesis / DynamoDB Streams to a function |
GetEventSourceMapping |
Get event source mapping details |
ListEventSourceMappings |
List all event source mappings |
UpdateEventSourceMapping |
Update a mapping |
DeleteEventSourceMapping |
Remove a mapping |
PublishVersion |
Publish an immutable version |
ListVersionsByFunction |
List all published versions of a function |
CreateAlias |
Create a named alias pointing to a version |
GetAlias |
Get alias details |
ListAliases |
List all aliases for a function |
UpdateAlias |
Update an alias |
DeleteAlias |
Delete an alias |
AddPermission |
Add a resource-policy statement |
GetPolicy |
Get the function resource policy |
RemovePermission |
Remove a resource-policy statement |
GetFunctionCodeSigningConfig |
Return code-signing config (always empty) |
CreateFunctionUrlConfig |
Provision a function URL |
GetFunctionUrlConfig |
Read function URL config |
UpdateFunctionUrlConfig |
Update function URL config |
DeleteFunctionUrlConfig |
Delete function URL config |
ListTags |
List tags on a function |
TagResource |
Tag a function |
UntagResource |
Untag a function |
PutFunctionConcurrency |
Set reserved concurrent executions |
GetFunctionConcurrency |
Get reserved concurrent executions |
DeleteFunctionConcurrency |
Clear reserved concurrent executions |
Hot-Reloading via Reactive S3 Sync
Floci supports an automatic hot-reloading mechanism when functions are deployed via S3. This follows the standard AWS behavior where S3 and Lambda interact, but is optimized for a seamless local development experience.
When a Lambda function is created using an S3 bucket and key, Floci maintains a link between the function and its source object. Any subsequent update to that S3 object (e.g., via s3:PutObject) automatically triggers a reactive synchronization:
- Detection: Floci detects the S3 update via an internal event system.
- Synchronization: The new code is automatically re-extracted to the local code storage.
- Invalidation: Any active "warm" containers for that function are proactively drained.
- Reload: The very next invocation starts a fresh container with the updated code.
This allows you to update your Lambda code by simply re-uploading your ZIP to S3, without having to manually call UpdateFunctionCode or restart any containers.
Example
# 1. Create a function linked to S3
aws lambda create-function \
--function-name my-function \
--code S3Bucket=my-bucket,S3Key=function.zip \
...
# 2. Invoke (starts a warm container)
aws lambda invoke --function-name my-function out.json
# 3. Update the code in S3 (Triggers Reactive Sync)
aws s3 cp updated-function.zip s3://my-bucket/function.zip
# 4. Invoke again (automatically picks up the new code)
aws lambda invoke --function-name my-function out.json
Standard Behavior
This mechanism requires no custom configuration or non-standard magic strings. It works with standard AWS SDKs and CLI tools, providing a "live" development feel while staying within the AWS API contract.
Hot-Reload via Bind Mount
For the tightest inner-loop development cycle, Floci supports a bind-mount hot-reload mode. Instead of packaging code into a ZIP and uploading it to S3, you point Floci directly at a directory on your host machine. The directory is bind-mounted into /var/task inside the container, so every invocation runs the files as they currently exist on disk — no upload, no redeploy.
This is enabled by using the magic bucket name hot-reload when creating a function:
aws lambda create-function \
--function-name my-function \
--runtime nodejs22.x \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler index.handler \
--code S3Bucket=hot-reload,S3Key=/absolute/path/to/your/code \
--endpoint-url http://localhost:4566
The S3Key must be an absolute path reachable by the Docker daemon. When Floci runs in Docker Compose, this is the path on the Docker host (the machine running Docker), not the path inside the Floci container.
How it works
CreateFunctionwithS3Bucket=hot-reloadmarks the function as a hot-reload function;S3Keyis stored as the host-side path.- On each invocation, Floci starts a fresh ephemeral container with the host path bind-mounted at
/var/task. - The container executes the files as they exist at invocation time — editing a file and immediately invoking picks up the change without any API call.
- After the invocation completes the container is stopped and removed, ensuring the next invocation always sees the current state of the directory.
Configuration
Hot-reload must be enabled explicitly. By default it is disabled so that S3Bucket=hot-reload is treated as a regular S3 bucket name.
FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED=true
# Optional: restrict which host paths may be bind-mounted (comma-separated)
FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ALLOWED_PATHS=/home/user/projects,/tmp
Docker Compose setup — enable the feature and share the Docker socket:
services:
floci:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED: "true"
Limitations
- The
S3Keypath is interpreted by the Docker daemon, not by Floci. When Floci itself runs inside Docker, the path must exist on the Docker host machine, not inside the Floci container. - Hot-reload containers are always ephemeral — there is no warm-container reuse. Each invocation pays a cold-start penalty.
UpdateFunctionCodeon a hot-reload function converts it back to a standard Zip function (the hot-reload bind-mount is removed).- S3 reactive sync is skipped for hot-reload functions — edits are picked up directly from disk.
Difference from Reactive S3 Sync
| Reactive S3 Sync | Bind-Mount Hot-Reload | |
|---|---|---|
| Trigger | Upload a new ZIP to S3 | Edit files on disk |
| Cold start | Only after upload | Every invocation |
| Requires upload step | Yes | No |
Works without hot-reload enabled |
Yes | No |
| Path on host required | No | Yes |
Concurrency enforcement
Reserved concurrency is enforced: invocations beyond the reserved value
return TooManyRequestsException (HTTP 429). Functions without a reserved
value share a per-region pool — AWS Lambda's "account-level" limit is
in fact a per-account-per-region quota, and Floci mirrors that by
partitioning counters on the ARN's region segment. The pool size (default
1000) is configurable via floci.services.lambda.region-concurrency-limit
and applies independently to each region. PutFunctionConcurrency
validates that the requested value leaves at least
floci.services.lambda.unreserved-concurrency-min (default 100) available
for unreserved functions in that region. PutProvisionedConcurrencyConfig
and related provisioned-concurrency operations remain unimplemented.
Reducing or clearing a function's reserved value does not kill
invocations that are already in flight — this matches AWS, which
applies changes only to new invocations. As a consequence, during the
drain window Σreserved-inflight + unreserved-inflight can briefly
exceed region-concurrency-limit.
Function URLs are also reachable directly on /{proxy:.*} under the Lambda URL controller, which routes the request into the normal Invoke path.
Stubbed: ListLayers and ListLayerVersions return empty arrays. No layer storage exists.
Not Implemented
These AWS Lambda operations have no handler in Floci. Calls will return 404 or an error:
- Layers (
PublishLayerVersion,DeleteLayerVersion,GetLayerVersion,GetLayerVersionByArn,AddLayerVersionPermission,RemoveLayerVersionPermission,GetLayerVersionPolicy) - Provisioned concurrency (
PutProvisionedConcurrencyConfig,GetProvisionedConcurrencyConfig,ListProvisionedConcurrencyConfigs,DeleteProvisionedConcurrencyConfig) - Dead-letter, async invoke config, and event invoke config operations
InvokeWithResponseStream- Code signing management (only
GetFunctionCodeSigningConfigis wired; there is noPutFunctionCodeSigningConfigorCreateCodeSigningConfig) - Account and regional settings (
GetAccountSettings)
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_LAMBDA_ENABLED |
true |
Enable or disable the service |
FLOCI_SERVICES_LAMBDA_EPHEMERAL |
false |
Remove containers after each invocation |
FLOCI_SERVICES_LAMBDA_DEFAULT_MEMORY_MB |
128 |
Default function memory (MB) |
FLOCI_SERVICES_LAMBDA_DEFAULT_TIMEOUT_SECONDS |
3 |
Default function timeout (seconds) |
FLOCI_SERVICES_LAMBDA_RUNTIME_API_BASE_PORT |
9200 |
First port in the Lambda Runtime API range |
FLOCI_SERVICES_LAMBDA_RUNTIME_API_MAX_PORT |
9299 |
Last port in the Lambda Runtime API range |
FLOCI_SERVICES_LAMBDA_CODE_PATH |
./data/lambda-code |
Directory where Lambda ZIP files are stored |
FLOCI_SERVICES_LAMBDA_POLL_INTERVAL_MS |
1000 |
Event-source mapping poll interval (milliseconds) |
FLOCI_SERVICES_LAMBDA_CONTAINER_IDLE_TIMEOUT_SECONDS |
300 |
Idle container shutdown timeout (seconds) |
FLOCI_SERVICES_LAMBDA_REGION_CONCURRENCY_LIMIT |
1000 |
Maximum concurrent executions per region |
FLOCI_SERVICES_LAMBDA_UNRESERVED_CONCURRENCY_MIN |
100 |
Minimum unreserved capacity PutFunctionConcurrency must leave |
FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED |
false |
Enable bind-mount hot-reload via S3Bucket=hot-reload |
FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ALLOWED_PATHS |
(unset) | Comma-separated allowlist of host paths that may be bind-mounted |
Docker socket requirement
Lambda requires the Docker socket. Mount it in your compose file:
S3 virtual-hosted-style addressing inside Lambda containers
AWS SDKs use virtual-hosted-style S3 addressing by default, forming URLs like
https://my-bucket.s3.amazonaws.com/key. Against Floci the same pattern becomes
http://my-bucket.localhost.floci.io:4566/key.
When Floci runs inside Docker, Lambda containers are on the same Docker
network. Docker's embedded DNS resolves the exact alias localhost.floci.io
correctly, but has no wildcard support — my-bucket.localhost.floci.io
falls through to public DNS and resolves to the wrong IP, causing the Lambda
invocation to time out.
Floci solves this automatically by running an embedded DNS server (UDP/53) on its container IP. All Lambda containers launched by Floci are configured to use it as their DNS resolver. The embedded DNS server:
- Resolves
*.localhost.floci.io→ Floci's Docker network IP - Forwards all other queries to the upstream resolver from
/etc/resolv.conf
No extra configuration or cap_add is needed — Docker containers have
CAP_NET_BIND_SERVICE in their default capability set, so Floci (running as a
non-root user) can bind UDP/53 without any changes to your Compose file.
Docker Compose service names
If Floci runs as a Docker Compose service, set FLOCI_HOSTNAME to the
service name, for example FLOCI_HOSTNAME=floci. When no explicit Lambda
Docker network is configured, Floci automatically attaches Lambda
containers to the current Compose network. Floci then injects
AWS_ENDPOINT_URL=http://floci:4566 into Lambda containers and returns
SQS QueueUrl values with the same reachable host.
This avoids function-side rewrites from localhost or localhost.floci.io
to floci, and keeps normal AWS SDK clients pointed at the Docker DNS name
that the Lambda container can resolve.
Path-style as a workaround
If you cannot use virtual-hosted-style (e.g. Floci is running natively on
the host, not in Docker), configure the SDK client with
forcePathStyle: true / s3ForcePathStyle: true. Requests will go to
http://localhost:4566/my-bucket/key instead and work without DNS.
Migrating from LocalStack
If your Lambda functions have AWS_ENDPOINT_URL=http://localhost.localstack.cloud:4566
hardcoded, add the LocalStack suffix to Floci's DNS resolver so it resolves to
Floci's IP without any function-side changes:
Via environment variable — use a comma-separated list for multiple suffixes:
# Single suffix
FLOCI_DNS_EXTRA_SUFFIXES=localhost.localstack.cloud
# Multiple suffixes
FLOCI_DNS_EXTRA_SUFFIXES=localhost.localstack.cloud,localhost.example.internal
Real AWS Credentials
By default, Floci injects placeholder credentials (test/test/test) into Lambda containers. This is sufficient when all SDK calls target Floci's emulated services.
For hybrid local/cloud testing — where some services are emulated and others hit real AWS — you can mount your host ~/.aws directory into Lambda containers:
services:
floci:
image: floci/floci:latest
environment:
FLOCI_SERVICES_LAMBDA_AWS_CONFIG_PATH: /Users/me/.aws
volumes:
- /var/run/docker.sock:/var/run/docker.sock
When aws-config-path is set:
- The host path is bind-mounted read-only into each Lambda container at
/opt/aws-config AWS_SHARED_CREDENTIALS_FILEandAWS_CONFIG_FILEenv vars are set so the SDK discovers credentials regardless of the container's HOME directory- No
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKENenv vars are injected
When unset (default), Floci reads credentials from its own environment and falls back to test/test/test.
Routing specific services to real AWS
To keep some services on Floci while others hit real AWS, clear the global endpoint and set service-specific overrides in your function's --environment:
AWS_ENDPOINT_URL= # clear Floci's global endpoint
AWS_ENDPOINT_URL_SES=http://localhost.floci.io:4566 # SES stays on Floci
AWS_ENDPOINT_URL_CLOUDWATCHLOGS=http://localhost.floci.io:4566 # CloudWatch stays on Floci
The AWS SDK supports AWS_ENDPOINT_URL_<SERVICE> natively. Services without an override will use real AWS endpoints.
Credential passthrough without mounting
If you don't need the full ~/.aws directory (e.g., you only have static credentials), you can pass them to Floci's environment directly. When aws-config-path is unset, Floci forwards its own AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN env vars into Lambda containers:
Private registry authentication
Container image functions ("PackageType": "Image") that pull from private registries need Docker credentials. See Docker Configuration → Private Registry Authentication for the full guide.
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
# Package a simple Node.js function
cat > index.mjs << 'EOF'
export const handler = async (event) => {
console.log("Event:", JSON.stringify(event));
return { statusCode: 200, body: JSON.stringify({ hello: "world" }) };
};
EOF
zip function.zip index.mjs
# Deploy the function
aws lambda create-function \
--function-name my-function \
--runtime nodejs22.x \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler index.handler \
--zip-file fileb://function.zip \
--endpoint-url $AWS_ENDPOINT_URL
# Invoke synchronously
aws lambda invoke \
--function-name my-function \
--payload '{"key":"value"}' \
--cli-binary-format raw-in-base64-out \
response.json \
--endpoint-url $AWS_ENDPOINT_URL
cat response.json
# Invoke asynchronously
aws lambda invoke \
--function-name my-function \
--invocation-type Event \
--payload '{"key":"value"}' \
--cli-binary-format raw-in-base64-out \
/dev/null \
--endpoint-url $AWS_ENDPOINT_URL
# Update code
zip function.zip index.mjs
aws lambda update-function-code \
--function-name my-function \
--zip-file fileb://function.zip \
--endpoint-url $AWS_ENDPOINT_URL
Event Source Mappings
Connect Lambda to SQS, Kinesis, or DynamoDB Streams:
# SQS trigger
QUEUE_ARN=$(aws sqs get-queue-attributes \
--queue-url $AWS_ENDPOINT_URL/000000000000/orders \
--attribute-names QueueArn \
--query Attributes.QueueArn --output text \
--endpoint-url $AWS_ENDPOINT_URL)
aws lambda create-event-source-mapping \
--function-name my-function \
--event-source-arn $QUEUE_ARN \
--batch-size 10 \
--endpoint-url $AWS_ENDPOINT_URL
ScalingConfig (SQS only)
CreateEventSourceMapping and UpdateEventSourceMapping accept a
ScalingConfig.MaximumConcurrency integer between 2 and 1000 on SQS
event sources, matching the AWS wire format. GetEventSourceMapping and
ListEventSourceMappings echo the value back when set; responses omit
the ScalingConfig field entirely when no cap is configured.
aws lambda create-event-source-mapping \
--function-name my-function \
--event-source-arn $QUEUE_ARN \
--scaling-config MaximumConcurrency=5 \
--endpoint-url $AWS_ENDPOINT_URL
Validation mirrors AWS: values outside 2–1000 are rejected with
InvalidParameterValueException, and ScalingConfig on a non-SQS event
source (Kinesis / DynamoDB Streams) is also rejected — those services
use ParallelizationFactor instead, which is a separate field.
Enforcement status
The configured MaximumConcurrency is persisted and returned on the
wire, but the SQS poller does not yet cap concurrent invocations at
this value (the poller today serializes invocations per ESM to one
at a time regardless). Real parallel dispatch capped by
MaximumConcurrency is tracked as a follow-up.
Supported Runtimes
Any runtime that has an official AWS Lambda container image works with Floci (e.g. nodejs22.x, python3.13, java21, go1.x, provided.al2023).