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) |
ListFunctionsByCodeSigningConfig |
Validates the ARN; no code-signing config can exist, so every well-formed ARN returns ResourceNotFoundException |
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 |
GetAccountSettings |
Account limits plus usage derived from the caller's stored functions |
PutFunctionEventInvokeConfig |
Set the asynchronous invocation settings of a function, version or alias (retries, event age, destinations) |
UpdateFunctionEventInvokeConfig |
Change some of those settings, leaving the rest as they are |
GetFunctionEventInvokeConfig |
Read the asynchronous invocation settings |
DeleteFunctionEventInvokeConfig |
Remove the asynchronous invocation settings |
ListFunctionEventInvokeConfigs |
List the asynchronous invocation settings of every version and alias of a function |
The event invoke configuration is stored and returned as AWS does, and AWS::Lambda::EventInvokeConfig
provisions it from a stack. Asynchronous invocations do not yet apply its retry, event age or
destination settings.
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, with no upload or 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, so 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 because 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.
Versions: CreateFunction and UpdateFunctionCode honour Publish, publishing a version
and reporting it in the response's Version. The two differ in the ARN they return, matching the
live service: CreateFunction keeps the unqualified FunctionArn while UpdateFunctionCode
returns the qualified one (...:function:name:2), as PublishVersion does. Without Publish
both answer for $LATEST and create nothing. UpdateFunctionConfiguration has no Publish
parameter in the AWS API and none here.
DeleteFunction honours Qualifier — it removes that published version only, leaving $LATEST,
the other versions and the function's aliases in place; without a qualifier the whole function
goes. Matching the live service, deleting $LATEST by qualifier and naming an alias are both
rejected with InvalidParameterValueException, a version an alias points at is a
ResourceConflictException, and a version that does not exist is a silent success rather than
a 404.
Layers: PublishLayerVersion, GetLayerVersion, GetLayerVersionByArn, ListLayerVersions,
ListLayers, and DeleteLayerVersion are implemented, with real local storage under
{lambda.codePath}/layers/{name}/{version}. ListLayers and ListLayerVersions honour
CompatibleRuntime, CompatibleArchitecture, MaxItems (1-50, defaulting to 50) and Marker,
and always emit NextMarker, null on the last page. Under a filter, LatestMatchingVersion is
the newest version that matches rather than the newest overall, and a layer with no matching
version is omitted; a version published without CompatibleArchitectures matches neither
architecture. Marker is opaque and signed with a key generated at startup, so a fabricated,
edited or previous-run token is rejected with InvalidParameterValueException rather than
applied as a cursor. One divergence: a parameter sent with an empty value
(?CompatibleRuntime=) is treated as absent rather than rejected, because RESTEasy binds an
empty query value as null. Resolution honours the ARN's
account and partition: an ARN naming another account resolves to nothing rather than to a
same-named layer of the caller's own, matching the live service, which answers that case with
AccessDeniedException and never substitutes. CreateFunction/UpdateFunctionConfiguration
validate each Layers ARN in the caller's own account eagerly against that storage, matching
real AWS - an unresolvable one is rejected with InvalidParameterValueException: Layer version
... does not exist., not silently accepted.
An ARN naming another account or another partition is answered on the live service by the layer's
resource policy: an AWS-managed public layer resolves, and everything else is
AccessDeniedException. Measured on CreateFunction in ap-southeast-1, a foreign-account ARN and
a cross-partition ARN return the same AccessDeniedException, so Floci returns that for both.
Floci implements no layer permissions and cannot fetch real AWS content, so it cannot tell a
public layer from a private one; refusing is the faithful default, being the answer AWS gives to
every foreign ARN except a public one.
Set floci.services.lambda.accept-external-layer-arns: true
(FLOCI_SERVICES_LAMBDA_ACCEPT_EXTERNAL_LAYER_ARNS) to record a same-partition foreign ARN on the
function instead of refusing it, which is what a stack attaching Powertools, the AppConfig
extension or a vendor-published layer needs. The trade is explicit: with it on, Floci also accepts
an ARN AWS would refuse with AccessDeniedException, so a typo or a private third-party layer
passes here and fails on deploy. The content is never mounted at /opt either way, and a warning
is logged at attach time and again at invoke; publish your own copy of the content locally under a
name you control if the handler needs it at runtime.
A layer ARN outside the aws partition is refused whatever that setting says. Partitions are
isolated, so no resource policy can ever make such a layer readable, and GetLayerVersionByArn
rejects one outright with InvalidParameterValueException: Invalid layer version ....
Not Implemented
These AWS Lambda operations have no handler in Floci. Calls will return 404 or an error:
- Layer permissions (
AddLayerVersionPermission,RemoveLayerVersionPermission,GetLayerVersionPolicy) - Provisioned concurrency (
PutProvisionedConcurrencyConfig,GetProvisionedConcurrencyConfig,ListProvisionedConcurrencyConfigs,DeleteProvisionedConcurrencyConfig) InvokeWithResponseStream- Code signing management (only
GetFunctionCodeSigningConfigandListFunctionsByCodeSigningConfigare wired; there is noPutFunctionCodeSigningConfigorCreateCodeSigningConfig, so no code-signing config can exist andListFunctionsByCodeSigningConfigreports every well-formed ARN asResourceNotFoundException— a malformed ARN or an out-of-rangeMaxItemsis rejected withInvalidParameterValueExceptionfirst)
Configuration
Lambda container architecture
Floci uses the Docker host architecture by default. Set
FLOCI_SERVICES_LAMBDA_HONOUR_ARCHITECTURES=true to run each function with
its declared arm64 or x86_64 architecture. The Docker host must support
the selected architecture. Foreign architectures require Docker Desktop or
host emulation such as binfmt_misc with QEMU. Floci does not fall back to
the host architecture when this setting is enabled.
| 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_HONOUR_ARCHITECTURES |
false |
Select each function's declared architecture for Docker image pulls and containers |
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 |
12000 |
First port in the Lambda Runtime API range |
FLOCI_SERVICES_LAMBDA_RUNTIME_API_MAX_PORT |
12499 |
Last port in the Lambda Runtime API range. One port is held per running container, so the range width caps concurrent executions |
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 |
FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK |
(unset) | Docker network to attach Lambda containers to (overrides FLOCI_SERVICES_DOCKER_NETWORK) |
FLOCI_SERVICES_LAMBDA_EXTRA_HOSTS |
(unset) | Comma-separated hostname:ip entries added to each Lambda container's /etc/hosts; ip may be host-gateway, mirroring docker run --add-host |
FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE |
(unset) | Explicit host/IP that spawned Lambda containers use to reach Floci's Runtime API, bypassing auto-detection |
FLOCI_SERVICES_LAMBDA_CONTAINER_NAME_PREFIX |
floci |
Base name prefix for spawned Lambda containers and code volumes (e.g. acme → acme-<function>-<id> containers, acme-code-<function>-<hash> volumes). Must be a valid Docker name segment ([A-Za-z0-9][A-Za-z0-9_.-]*); invalid values are ignored with a warning |
FLOCI_SERVICES_LAMBDA_CODE_VOLUME_POPULATE_CONCURRENCY |
max(2, cpus/2) |
Maximum concurrent first-time code-volume populates. See the note below |
FLOCI_SERVICES_LAMBDA_EXECUTOR |
docker |
Execution backend: docker (containers) or kubernetes (pods) |
FLOCI_SERVICES_LAMBDA_KUBERNETES_NAMESPACE |
default |
Namespace Lambda pods are created in |
FLOCI_SERVICES_LAMBDA_KUBERNETES_LABELS |
(unset) | Extra pod labels as comma-separated key=value entries |
FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS |
(unset) | Host/IP pods use to reach Floci; auto-detected when Floci runs in-cluster |
FLOCI_SERVICES_LAMBDA_KUBERNETES_INIT_IMAGE |
busybox:1.36 |
Init-container image that downloads function code (needs sh, wget, unzip) |
Changing the container name prefix
Code volumes are resolved by name, and a Floci process only manages resources under its
own prefix — deliberately, so multiple Floci processes with different prefixes can share
one Docker daemon without touching each other's containers and volumes. Restarting with a
different container-name-prefix therefore strands the code volumes (and their completion
markers) created under the previous prefix: they are no longer reused and no longer part of
automatic superseded-volume cleanup. They keep the prefix-independent floci=true label,
so reclaim them at any time with:
Concurrent cold starts of large functions
A function whose unpacked code is at least 32 MB has that code streamed once into a
read-only Docker volume, so later cold starts mount it instead of copying. Those
first-time populates are capped — a burst of them overwhelms the Docker daemon — and
the default cap is max(2, cpus/2), derived from the CPU count the JVM sees.
Because the JVM honours the container's cgroup CPU quota, running Floci with a small CPU allocation collapses the cap to 2, and a burst of cold starts across distinct large functions completes in waves of two rather than in parallel. Six such functions invoked at once take roughly three times the wall-clock of one. Raise the cap to decouple it from the CPU allocation:
Only first-time populates are gated. Warm containers, already-populated volumes, and functions under 32 MB are never serialised by this.
Runtime API host override
When a Lambda container starts, it calls back into Floci's Runtime API to fetch
events and post results. Floci auto-detects the address containers should use
for that callback (its own container IP on the shared network, or
host.docker.internal when running on the host). In most setups this is
correct and needs no configuration.
On unusual network topologies, for example rootless Podman, auto-detection
can pick an address the Lambda container cannot reach, and invocations fail with
connect ECONNREFUSED <ip>:12000. Set FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE
to the host or IP that containers can actually reach Floci on, and Floci uses it
verbatim instead of auto-detecting:
See Docker Configuration → Running on Podman (rootless) for a full rootless Podman walkthrough.
Docker socket requirement
With the default docker executor, Lambda requires the Docker socket. Mount it
in your compose file:
If mounting the Docker socket is not acceptable (for example on a hardened Kubernetes cluster), use the Kubernetes executor instead — it needs no privileged access and no socket.
Kubernetes executor
Set FLOCI_SERVICES_LAMBDA_EXECUTOR=kubernetes to run each Lambda execution
environment as a Kubernetes pod instead of a Docker container. This is designed
for CI/CD clusters where privileged containers and docker.sock access are not
allowed. Floci talks to the cluster through the standard Kubernetes API: when
running inside the cluster it uses its ServiceAccount, and when running outside
it uses your local kubeconfig. An inline static bearer token, a
client-certificate/client-key credential with a PKCS#8 private key, or the
aws eks get-token --cluster-name <name> [--region <region>] exec plugin
(what aws eks update-kubeconfig generates) is supported. --role-arn,
tokenFile, any other exec command, and auth-provider credential plugins
(gcloud, etc.) are not, and fail with a named error.
How an invocation works:
- On a cold start Floci creates a pod from the function's runtime image
(
public.ecr.aws/lambda/*, same mapping as the Docker executor). - An init container (
busyboxby default) downloads the function's deployment package — and any layers — from Floci's S3 over HTTP and unpacks them into sharedemptyDirvolumes at/var/taskand/opt. - The runtime container polls Floci's Lambda Runtime API
(
AWS_LAMBDA_RUNTIME_API) exactly like a Docker container would. - Warm pods are reused across invocations and reaped after
FLOCI_SERVICES_LAMBDA_CONTAINER_IDLE_TIMEOUT_SECONDSof inactivity. Pods left behind by a crashed Floci are swept on startup via theapp.kubernetes.io/managed-by=flocilabel.
Lambda pods connect back to Floci on the main port (4566) and the Runtime
API port range (9200–9299), so those ports must be reachable from pods in the
namespace. When Floci runs in-cluster this works out of the box (pod-to-pod
traffic); when Floci runs outside the cluster, set
FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS to an address the cluster's
pods can reach (for example your machine's LAN IP for a kind cluster).
Required RBAC
The ServiceAccount Floci runs under needs these permissions in the Lambda namespace. The manifest below is complete, so applying it as-is (together with the Deployment in the next section) yields a working setup:
apiVersion: v1
kind: ServiceAccount
metadata:
name: floci
namespace: floci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: floci-lambda
namespace: floci
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "get", "list", "watch", "delete", "deletecollection"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "watch"]
# Only needed when FLOCI_TLS_ENABLED=true (the CA bundle is shared via a ConfigMap)
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: floci-lambda
namespace: floci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: floci-lambda
subjects:
- kind: ServiceAccount
name: floci
namespace: floci
Running Floci in-cluster
A minimal Deployment (namespace floci assumed, RBAC from above bound to the
floci ServiceAccount):
apiVersion: apps/v1
kind: Deployment
metadata:
name: floci
spec:
replicas: 1
selector:
matchLabels: { app: floci }
template:
metadata:
labels: { app: floci }
spec:
serviceAccountName: floci
# Required when a Service named `floci` exists in the namespace: service
# links inject FLOCI_PORT=tcp://<ip>:4566, which collides with Floci's
# FLOCI_* configuration convention and fails startup.
enableServiceLinks: false
containers:
- name: floci
image: floci/floci:latest
env:
- name: FLOCI_SERVICES_LAMBDA_EXECUTOR
value: kubernetes
- name: FLOCI_SERVICES_LAMBDA_KUBERNETES_NAMESPACE
value: floci
ports:
- containerPort: 4566
---
apiVersion: v1
kind: Service
metadata:
name: floci
spec:
selector: { app: floci }
ports:
- port: 4566
targetPort: 4566
Lambda pods reach Floci by pod IP, so the Runtime API ports need no Service entries; only clients of the emulator itself use port 4566.
Limitations
- Hot reload (both bind-mount and Reactive S3 Sync bind variants) and
FLOCI_SERVICES_LAMBDA_AWS_CONFIG_PATHrely on bind mounts and are not supported; hot-reload functions fail to launch with a clear error. Imagepackage type URIs are passed to the kubelet unchanged. Images in Floci's emulated ECR registry are not pullable by cluster nodes — pre-load them onto the nodes (e.g.kind load docker-image) or use a real registry.- Lambda pods carry no
imagePullSecrets. If your runtime or init images come from an authenticated registry, attach the pull secret to the namespace'sdefaultServiceAccount (kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<secret>"}]}') so the kubelet uses it for every pod in the namespace. - A cold start waits up to 300 seconds for the pod to reach
Running(broken images and failing init containers are detected and reported much earlier). - The init container downloads code and layers over plain HTTP on the
emulator port even when
FLOCI_TLS_ENABLED=true. busyboxwgetcannot complete a TLS handshake with Floci, and the Runtime API traffic on the same pod network is plain HTTP anyway. Use a NetworkPolicy if the pod network is part of your threat model. - With
FLOCI_TLS_ENABLED=true, AWS SDK calls made from inside the function fail TLS verification when pods reach Floci by pod IP, because the self-signed certificate carries no SAN for dynamic pod IPs. SetFLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESSto a hostname covered by the certificate, or provide your own certificate viafloci.tls.cert-path. - Prefer an IP for
FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS. With a bare hostname, S3 SDKs inside functions may use virtual-hosted-style addressing (bucket.<hostname>), and nothing in the cluster resolves those subdomains. - IPv6-only clusters are not supported. Floci advertises an IPv4 address to pods and fails fast when none is available.
- Each Floci instance sweeps all
managed-by=flociLambda pods in its namespace at startup; run multiple Floci instances in separate namespaces. - Layers published by Floci versions before this feature have no stored archive; re-publish them once to make them downloadable by pods.
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(s) from
/etc/resolv.conf, falling back to public resolvers so public hostnames (e.g.business-api.tiktok.com) resolve from inside Lambda containers
No extra configuration or cap_add is needed because 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.
VpcConfig, SnapStart and LoggingConfig
All three round-trip through CreateFunction, UpdateFunctionConfiguration,
GetFunctionConfiguration, GetFunction, ListFunctions and PublishVersion.
The response shapes are not the request shapes, and Floci follows the AWS model rather than echoing the request back:
| Field | Request shape | Response shape | Extra members Floci fills in |
|---|---|---|---|
VpcConfig |
VpcConfig |
VpcConfigResponse |
VpcId, resolved from the first subnet via EC2 |
SnapStart |
SnapStart |
SnapStartResponse |
OptimizationStatus — On only for a published version with ApplyOn=PublishedVersions, Off for $LATEST |
LoggingConfig |
LoggingConfig |
LoggingConfig |
— |
SnapStart and LoggingConfig are always present in a response, as on AWS: an
unset function reads back SnapStart={ApplyOn: None, OptimizationStatus: Off} and
LoggingConfig={LogFormat: Text, LogGroup: /aws/lambda/<name>}. With
LogFormat=JSON, ApplicationLogLevel and SystemLogLevel are also returned,
defaulting to INFO. Terraform treats these as Computed blocks, so a missing one
is a permanent diff rather than a cosmetic omission.
LoggingConfig is replaced wholesale on update, not merged — an update naming only
LogFormat resets LogGroup to the default.
LogGroup is validated against AWS's documented constraint: 1-512 characters matching
[.\-_/#A-Za-z0-9]+. ApplicationLogLevel and SystemLogLevel are accepted with any
LogFormat but are only ever stored — and therefore only ever returned — when the
resolved format is JSON; supplying them with LogFormat=Text is not an error, it is
simply a no-op. That is Floci's own call rather than probed AWS behaviour: it keeps the
request path consistent with Floci's response shape, which never surfaces the levels for
Text.
VpcConfig is omitted entirely while the function is not attached to a VPC.
Subnets that EC2 does not know about are still accepted and returned; only VpcId
is left off in that case.
RuntimeVersionConfig.RuntimeVersionArn is returned for managed (non-image)
runtimes. Its value is derived from the runtime name, so it is stable across
restarts.
File system configs
FileSystemConfigs accepts one EFS access point and mounts it under the
requested /mnt/... path for local Lambda containers. As on AWS, the function
must include VPC subnet and security group configuration. The mounted path uses
the same shared-volume initialization settings as ECS EFS volumes under
floci.storage.efs.
This configuration is supported through the Lambda API, AWS::Lambda::Function
resources, and AWS::Serverless::Function resources.
S3 Files access points are not currently emulated and are rejected instead of being mounted as an empty local volume.
Resolving public hostnames from Lambda
A Lambda whose handler reaches a public host (fetch()/HTTPS to e.g.
business-api.tiktok.com) resolves it through Floci's embedded DNS. As a
safety net, Floci also appends configurable public resolvers (default
8.8.8.8, 8.8.4.4) after its own IP on each spawned container's DNS list,
so name resolution still works if the embedded forwarder cannot answer.
Tune or disable this for offline / locked-down networks where those resolvers are blocked:
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, a Lambda function whose execution role exists in Floci's IAM store receives temporary credentials for that role. SDK calls made by the function identify as assumed-role/<role>/floci-session, and IAM enforcement evaluates the role's policies. If the role is unknown to Floci, the container keeps the compatibility fallback described below.
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 injects execution-role credentials for a known role. For an unknown role, 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
For functions whose execution role is unknown to Floci, you can pass static credentials 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 those Lambda containers:
environment:
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
A known execution role takes precedence over AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN values in the function environment. Use aws-config-path when the function must use mounted credentials instead of its emulated execution role.
Passthrough is on whenever those three variables are set in Floci's own environment, so a
Floci started from a shell that exports real AWS credentials (aws-vault exec, a sourced
credentials file, a CI runner) hands them to any function whose role it does not know. An
AWS_PROFILE or an aws sso login alone does not do this: those populate config and cache
files, not the environment. Floci logs a WARN carrying the forwarded access-key prefix the
first time it happens. Give the function a role Floci knows, or set aws-config-path, to keep
host credentials out of the container.
Locally built images
A container image function whose ImageUri names an image already present on the Docker daemon runs that image directly. This includes AWS-shaped <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag> URIs, which are otherwise rewritten to Floci's emulated ECR registry at pull time: build the image under the exact URI the function declares (docker build -t <ImageUri> .) and no push is needed. With FLOCI_DOCKER_IMAGE_REGISTRY_BASE set, tag it as <base>/<ImageUri> instead, since that is the reference Floci launches. Set FLOCI_SERVICES_ECR_PREFER_LOCAL_IMAGES=false to always resolve AWS-shaped URIs through the emulated registry.
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. Self-managed Apache Kafka event source mappings are accepted, validated, persisted, and returned on the wire, but Floci does not run an active Kafka consumer poller:
For DynamoDB Streams mappings, Floci retries failed batches with exponential backoff, honors MaximumRetryAttempts and MaximumRecordAgeInSeconds, and sends discarded batches to configured SQS or SNS DestinationConfig.OnFailure destinations.
# 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
MaximumBatchingWindowInSeconds (SQS)
CreateEventSourceMapping and UpdateEventSourceMapping accept a
MaximumBatchingWindowInSeconds integer between 0 and 300. GetEventSourceMapping
and ListEventSourceMappings echo it back when set; responses omit the field when
it was never configured. Values outside 0 to 300 are rejected with
InvalidParameterValueException.
When the window is greater than 0, the SQS poller holds an underfilled batch open,
accumulating messages across polls, and invokes the function once the batch reaches
BatchSize or the window elapses since the first buffered message, whichever comes
first. A window of 0 (or an unset window) invokes as soon as any message is
available, which is the previous behaviour.
aws lambda create-event-source-mapping \
--function-name my-function \
--event-source-arn $QUEUE_ARN \
--batch-size 10 \
--maximum-batching-window-in-seconds 5 \
--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.
FilterCriteria
CreateEventSourceMapping and UpdateEventSourceMapping accept a
FilterCriteria with up to 5 Filters, each carrying an event-pattern
Pattern, using the same EventBridge pattern syntax as EventBridge Pipes.
GetEventSourceMapping and ListEventSourceMappings echo it back when set and
omit the field entirely when unset. Filters are enforced in the Kinesis,
DynamoDB Streams, and SQS pollers: only matching records are delivered to the
function.
aws lambda create-event-source-mapping \
--function-name my-function \
--event-source-arn $QUEUE_ARN \
--filter-criteria '{"Filters":[{"Pattern":"{\"body\":{\"type\":[\"order\"]}}"}]}' \
--endpoint-url $AWS_ENDPOINT_URL
A pattern addresses each record the way its source presents it, matching AWS:
| Source | Filter key | Notes |
|---|---|---|
| SQS | body (+ messageAttributes, etc.) |
A JSON body is matched structurally; a non-JSON body cannot satisfy an object pattern and is dropped. Other top-level record fields (e.g. messageId, messageAttributes) are also addressable. |
| DynamoDB Streams | dynamodb (+ eventName, etc.) |
Matches the AttributeValue-wrapped image directly. Numeric operators do not apply (AttributeValue numbers are JSON strings), matching AWS. |
| Kinesis | data (+ partitionKey) |
data is matched against the base64-decoded payload (the delivered event still carries data base64-encoded); partitionKey is the supported metadata key. |
Non-matching records are consumed, not retried: Kinesis and DynamoDB Streams advance the shard iterator past them (a fully filtered batch still advances the checkpoint, so a shard never stalls), and SQS deletes filtered-out messages from the queue.
Validation mirrors AWS and runs before the mapping is stored: each Pattern
must be a JSON object whose field values are non-empty match arrays or nested
objects, at most 5 Filters, and each Pattern at most 4096 characters:
violations are rejected with InvalidParameterValueException. A FilterCriteria
of {} or with an empty Filters array clears any existing filters.
Supported operators
Filtering reuses Floci's shared EventBridge matcher (the same one EventBridge
Pipes uses). It supports exact match on a string, number or null, plus
prefix, suffix, equals-ignore-case, exists, anything-but (a string,
a non-empty array of strings and numbers, or a nested prefix), and numeric
comparison/value pairs using =, >, >=, <, <=. Patterns using only
these behave as on AWS.
A pattern is validated at CreateEventSourceMapping and
UpdateEventSourceMapping and rejected with InvalidParameterValueException
when the matcher could not satisfy it: an unknown operator, an operator AWS
documents that Floci does not implement (cidr, wildcard), more than one
operator in a single match element, a boolean literal, a wrong operand type,
or a malformed numeric sequence such as an odd-length array or a
non-numeric value. This is stricter than AWS, which accepts several of these.
The trade is deliberate: the pollers checkpoint past (Kinesis, DynamoDB) or
delete (SQS) any record a pattern fails to match, so a pattern that cannot
match destroys records rather than being inert, and create time is the last
point at which the caller can still act on it.
One matcher deviation remains and is not a validation error, because it depends on the record rather than the pattern: event-value array intersection, where AWS matches when the record's own field is itself an array and any element satisfies the pattern, and Floci does not.
Direct Lambda API only
FilterCriteria is carried only by the direct Lambda
CreateEventSourceMapping / UpdateEventSourceMapping APIs (SDK, CLI,
Terraform). CloudFormation and SAM event-source-mapping resources do not yet
forward FilterCriteria (as they also do not forward ScalingConfig or
DestinationConfig); forwarding it through those paths 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).