Running Bun and Alternative JavaScript Runtimes on AWS Lambda
Run Bun and Deno on AWS Lambda with custom runtimes: performance benchmarks, cost analysis, and production deployment patterns.
AWS Lambda officially supports Node.js, but the platform’s custom runtime capability opens the door to alternative JavaScript runtimes like Bun and Deno. Two mechanisms make that work: Lambda Layers and container images. Both give up the initialization tuning AWS applies to its own managed runtimes, and that trade lands on cold starts.
For most Lambda workloads the managed Node.js runtime stays the right default. An alternative runtime earns its slot only when a measured constraint justifies it, and in that case a container image with a pre-warmed Deno cache is the more predictable of the two paths.
The Custom Runtime Question
Alternative JavaScript runtimes become attractive in a few recurring situations: cold start overhead in latency-sensitive applications, avoiding a TypeScript transpilation step, CPU-bound workloads where runtime efficiency shows up on the bill, and access to modern JavaScript features ahead of Node.js LTS support.
The core trade-off: AWS Lambda is heavily optimized for its managed runtimes, and a custom runtime gives those optimizations up. Any performance gain has to cover both the cold start penalty and the implementation cost.
Understanding Lambda Custom Runtimes
AWS Lambda’s custom runtime feature allows you to run any runtime by implementing the Lambda Runtime API. This API provides a simple HTTP interface that your runtime uses to receive events and return responses.
The Runtime API Flow
// Simplified Lambda Runtime API implementation
const RUNTIME_API = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime`;
while (true) {
// 1. Get next invocation
const eventResponse = await fetch(`${RUNTIME_API}/invocation/next`);
const requestId = eventResponse.headers.get('Lambda-Runtime-Aws-Request-Id');
const event = await eventResponse.json();
try {
// 2. Invoke handler
const result = await handler(event);
// 3. Return response
await fetch(`${RUNTIME_API}/invocation/${requestId}/response`, {
method: 'POST',
body: JSON.stringify(result),
});
} catch (error) {
// 4. Report error
await fetch(`${RUNTIME_API}/invocation/${requestId}/error`, {
method: 'POST',
body: JSON.stringify({
errorMessage: error.message,
errorType: error.constructor.name,
}),
});
}
}
The bootstrap process runs in an infinite loop, requesting events from Lambda, executing your handler, and returning results. This simple protocol is what makes custom runtimes possible.
Implementation Approach 1: Bun with Lambda Layers
Lambda Layers provide a way to package and share runtime dependencies across multiple functions. Bun maintains an official bun-lambda package that implements the Runtime API.
Building the Bun Lambda Layer
# Clone Bun repository
git clone https://github.com/oven-sh/bun.git
cd bun/packages/bun-lambda
# Build and publish layer (architecture defaults to aarch64)
bun run publish-layer
# Build for x86_64 (recommended for compatibility)
bun run publish-layer -- --arch x64
The --arch flag accepts x64 or aarch64 only; arm64 is not a valid value even though that is what the Lambda console calls the architecture.
The publish script creates a Lambda Layer with the Bun runtime and bootstrap script, then publishes it to your AWS account. You’ll get back a Layer ARN that looks like arn:aws:lambda:us-east-1:123456789012:layer:bun-runtime:1.
Writing a Bun Lambda Handler
Bun Lambda handlers follow the Web API standard instead of Node.js conventions:
// handler.ts - Bun Lambda handler
export default {
async fetch(request: Request): Promise<Response> {
const event = await request.json();
// Process Lambda event
const result = {
message: 'Hello from Bun on Lambda!',
timestamp: Date.now(),
input: event,
};
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' },
});
},
};
Notice the handler exports a fetch method, not handler. This follows Bun’s Web API approach. Lambda events are converted to standard Request objects, and your handler returns Response objects.
Deploying with AWS CDK
import { Function, Runtime, Code, LayerVersion, Architecture } from 'aws-cdk-lib/aws-lambda';
// Reference the published Bun layer
const bunRuntimeLayer = LayerVersion.fromLayerVersionArn(
this,
'BunRuntime',
'arn:aws:lambda:us-east-1:123456789012:layer:bun-runtime:1'
);
const bunFunction = new Function(this, 'BunFunction', {
runtime: Runtime.PROVIDED_AL2023,
handler: 'index.fetch',
code: Code.fromAsset('dist'),
layers: [bunRuntimeLayer],
architecture: Architecture.X86_64, // Must match layer architecture
});
Critical requirement: The layer architecture must match the function architecture. Build separate layers for x86_64 and arm64 if you need both.
Implementation Approach 2: Container Images
Container images provide full control over the runtime environment and enable advanced optimizations. This approach uses the AWS Lambda Web Adapter to convert HTTP servers into Lambda-compatible handlers.
Bun Container Deployment
# Multi-stage build for Bun Lambda deployment
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS aws-lambda-adapter
FROM oven/bun:1-debian AS runtime
# Copy Lambda adapter
COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /var/task
# Install dependencies
COPY package.json bun.lock ./
RUN bun install --production --frozen-lockfile
# Copy application
COPY . .
# Serve on the port the adapter forwards to by default
ENV PORT=8080
CMD ["bun", "run", "index.ts"]
The Lambda adapter intercepts incoming Lambda events, converts them to HTTP requests to your server on port 8080, then converts responses back to Lambda format.
Deno with Cache Pre-warming
Deno’s architecture caches module resolution and compilation. Pre-running the application during the Docker build populates these caches:
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter
FROM denoland/deno:bin-2.6.3 AS deno-bin
FROM debian:bookworm-slim
# Install Deno
COPY --from=deno-bin /deno /usr/local/bin/deno
COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /var/task
ENV DENO_DIR=/var/deno_dir
# Copy application
COPY . .
# Critical: Pre-warm Deno caches
# This runs the app once during build to populate runtime caches
RUN timeout 10s deno run --allow-net main.ts || [ $? -eq 124 ] || exit 1
ENV PORT=8080
CMD ["deno", "run", "--allow-net", "main.ts"]
The timeout 10s command runs the application during build, letting Deno cache all module resolution and compilation. Exit code 124 (timeout) is expected and acceptable; the goal here is a populated cache, not a running server.
Building and Deploying Container Images
# Build for correct architecture (critical on Apple Silicon)
docker build \
--platform linux/amd64 \
--provenance=false \
-t bun-lambda:latest .
# Authenticate to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin ${ECR_URI}
# Tag and push
docker tag bun-lambda:latest ${ECR_URI}:latest
docker push ${ECR_URI}:latest
# Create Lambda function
aws lambda create-function \
--function-name bun-container-function \
--package-type Image \
--code ImageUri=${ECR_URI}:latest \
--role arn:aws:iam::123456789012:role/lambda-role
Platform specification is critical: Lambda defaults to x86_64, but Docker on Apple Silicon defaults to arm64. Always specify --platform linux/amd64 unless you’re using arm64 Lambda functions.
Performance Benchmarks
Two published benchmarks cover this ground and they rank the runtimes differently. Read side by side, they explain more than either does alone.
Initialization and Invocation at 128 MB
Jason Butz published a JavaScript runtime comparison on Lambda in May 2025 and left the CDK stack on GitHub, so the configuration is open to inspection: us-east-2, 128 MB, arm64, one SQS-triggered function per runtime fired by an EventBridge rule every three hours, 1,326 invocations and roughly 70 cold starts per runtime, 3,978 invocations in total. Each invocation computes 50 SHA3-512 hashes. Node.js runs on the managed nodejs22.x zip runtime, Bun on provided.al2023 with an aarch64 layer, Deno as a container image on denoland/deno:bin-1.45.2 behind the Lambda Web Adapter.
Initialization duration as Butz reports it:
| Runtime | Packaging | Average | p10 | p90 |
|---|---|---|---|---|
| Node.js 22 | Managed zip | 152.014ms | 145.555ms | 159.869ms |
| Deno 1.45.2 | Container image | 267.474ms | 184.607ms | 297.237ms |
| Bun | Layer on provided.al2023 | 547.651ms | 500.075ms | 603.223ms |
On those averages the Deno container initializes 76% slower than the managed Node.js runtime (267.474 / 152.014 = 1.76) and the Bun layer 260% slower (547.651 / 152.014 = 3.60). Stated the other way, Node.js initializes 43% below the Deno container and 72% below the Bun layer.
Invocation duration from the same run:
| Runtime | Average | p50 | p90 |
|---|---|---|---|
| Deno 1.45.2 | 13.708ms | 6.692ms | 19.836ms |
| Node.js 22 | 21.290ms | 8.052ms | 56.711ms |
| Bun | 50.513ms | 15.190ms | 68.230ms |
Warm invocations reverse the order. Deno’s average sits 36% below Node.js and its p90 is the tightest of the three, even though it runs as a container-based custom runtime.
One constraint governs the whole table: these functions had 128 MB. AWS documents that Lambda allocates CPU power in proportion to configured memory, and that a function reaches the equivalent of one vCPU at 1,769 MB. A SHA3-512 loop at 128 MB therefore runs on roughly a fourteenth of a vCPU. The figures describe one severely CPU-constrained configuration.
The Same Runtimes as Containers
Deno published a cold start study in July 2024 with a different setup: 512 MB, us-west-2, x86_64, and all three runtimes packaged as container images behind the Lambda Web Adapter, 20 to 25 forced cold starts per combination, on Deno 1.45.2, Bun 1.1.19 and Node.js 22.5.1. It is a vendor benchmark of the vendor’s own runtime, so weigh the ranking accordingly; the raw data and the harness are published alongside it.
Mean initialization duration by HTTP framework:
| Framework | Deno | Bun | Node.js |
|---|---|---|---|
| Hono | 57.6ms | 98.6ms | 102.0ms |
| Express | 134.9ms | 178.8ms | 183.7ms |
| Fastify | 187.3ms | 273.0ms | 261.1ms |
Node.js is slowest on Hono and Express and second slowest on Fastify. Bun lands between 98.6ms and 273.0ms, nowhere near the 547.651ms of the layer path.
What the Disagreement Shows
Each study on its own reads as a ranking of runtimes. Side by side they point at packaging, the one variable neither of them isolates.
Node.js leads the first study, and it is the only entry there running on AWS’s managed zip runtime. Once it is containerised like everything else in the second study, it is slowest on two frameworks out of three. Bun’s 547.651ms comes from the layer path on provided.al2023, while the second study puts containerised Bun under 100ms on Hono. Neither study separates runtime from packaging. In the first, each runtime carries a different packaging, so the two move together. The second holds packaging constant across runtimes but differs from the first in memory, region, architecture, framework and workload, so the distance between 547.651ms and 98.6ms cannot be charged to packaging either. What the pair supports is a hypothesis: on initialization duration, how a runtime is packaged may weigh as much as which runtime it is. Establishing it takes one runtime and one workload packaged each of the three ways at the same memory, region and architecture: a zip deployment package, a layer on provided.al2023, and a container image. No published benchmark does that, so the ordering stays untested.
Cost Analysis
Lambda bills requests and duration separately. AWS lists requests at 0.20 per million), identical on x86 and arm64, and duration in GB-seconds rounded up to the nearest millisecond. In us-east-1 the first duration tier is 0.0000133334 on arm64.
Since 1 August 2025 Lambda bills the INIT phase on every function configuration. AWS notes that custom runtimes, provisioned concurrency and OCI packaging already included INIT in billed duration before that change, so a slow custom-runtime cold start has always reached the invoice.
The second input is how often cold starts happen. AWS documents that they typically occur in under 1% of invocations, with durations from under 100ms to over a second. That number describes the platform as a whole, not a ceiling for any one function. Butz’s own run logged roughly 70 cold starts in 1,326 invocations per runtime, about 5%, because a three-hour schedule lets execution environments go idle between firings. Infrequent and bursty functions land above 1% for the same reason. Measure the rate on your own function, because the initialization line in the model below moves with it.
Working the Numbers
Use Butz’s configuration so that the milliseconds and the rate describe the same function: 128 MB on arm64, first tier. That is 0.0000000016667 per millisecond. The cold start rate is the free variable, so fix it at 1% for the table: of 10 million invocations per month, 100,000 pay an initialization charge. The warm invocation rows apply to all 10 million, and they carry Butz’s SHA3-512 workload with them.
| Difference from Node.js | Per event | Monthly at 10M invocations |
|---|---|---|
| Bun layer initialization, 395.637ms slower | $0.00000066 added | $0.07 added |
| Deno container initialization, 115.460ms slower | $0.00000019 added | $0.02 added |
| Bun warm invocation, 29.223ms slower | $0.0000000487 added | $0.49 added |
| Deno warm invocation, 7.582ms faster | $0.0000000126 saved | $0.13 saved |
The request charge alone on those 10 million invocations is 0.33; at 10% it is $0.66 and overtakes its own warm row. Across that range the runtime choice moves the bill by about a dollar in either direction. The case for staying on the managed runtime rests on cold start latency and on the operational surface of a runtime you maintain yourself.
The Bigger Lever: Architecture
Changing architecture saves more than changing runtime, and leaves less to maintain. AWS prices arm64 duration at 0.0000166667 on x86, a 20% reduction across the whole duration line. The arm64 first tier is also 25% wider (7.5 billion GB-seconds against 6 billion), so it stays at the cheapest rate longer. Requests cost the same on both, and AWS states that all supported Lambda runtimes support both x86_64 and arm64.
What actually decides it:
- Steady traffic keeps execution environments warm, so Deno’s shorter invocation duration shows up in latency percentiles while the money stays at cents
- Bursty traffic pays the initialization difference on every scale-out, and that number is user-facing
- CPU-bound work above 128 MB needs its own measurement, because CPU allocation scales with configured memory
- I/O-bound functions, the majority of Lambda workloads, spend their time waiting on the network, where the runtime moves neither number much
Common Pitfalls
Platform Architecture Mismatch
Building container images for the wrong CPU architecture causes cryptic runtime errors.
Symptom:
Error: Runtime exited with error: exit status 1
Runtime.InvalidEntrypoint
Root cause: Lambda defaults to x86_64, but Docker on Apple Silicon defaults to arm64.
Solution:
# Always specify platform in build
docker build --platform linux/amd64 -t myfunction .
# Verify built image
docker inspect myimage:latest | grep Architecture
# Should output: "Architecture": "amd64"
Missing Lambda Adapter Configuration
Container runs locally but fails on Lambda with connection errors.
Symptom: Function times out or returns 502 Bad Gateway.
Root cause: The adapter forwards traffic to port 8080 unless told otherwise, so a server bound to 3000 never receives it. The target is set by AWS_LWA_PORT, which falls back to PORT; that fallback is why most examples set PORT alone.
Correct implementation:
# 8080 is the adapter's default target, so the server has to listen there
ENV PORT=8080
CMD ["bun", "run", "server.ts"]
To serve on a different port, set AWS_LWA_PORT to the same value the application binds to.
// Use environment variable in application
const port = process.env.PORT || 3000;
Bun.serve({
port: Number(port),
fetch(request) {
return new Response('Hello World');
}
});
AWS SDK Compatibility Issues
Earlier Bun versions had AWS SDK compatibility challenges including Could not resolve: 'http2' errors and SignatureDoesNotMatch errors with S3. Recent versions have improved significantly, but always test AWS SDK operations explicitly in your specific use case:
// test/aws-sdk.test.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { describe, test, expect } from 'bun:test';
describe('AWS SDK Compatibility', () => {
test('S3 PutObject works', async () => {
const client = new S3Client({ region: 'us-east-1' });
const result = await client.send(new PutObjectCommand({
Bucket: 'test-bucket',
Key: 'test.txt',
Body: 'test content'
}));
expect(result.$metadata.httpStatusCode).toBe(200);
});
});
Pin Bun version in Dockerfile:
# Use specific version tag for stability
FROM oven/bun:1-debian
Lambda Layer Architecture Mismatch
Problem: Layer deploys successfully but function fails with “Runtime not supported” error.
Solution: Build and publish layers for both architectures:
# Build for x86_64
bun run publish-layer -- --arch x64
# Output: arn:aws:lambda:us-east-1:123:layer:bun-x64:1
# Build for arm64 (the flag value is aarch64)
bun run publish-layer -- --arch aarch64
# Output: arn:aws:lambda:us-east-1:123:layer:bun-arm64:1
Match architecture between layer and function in CDK:
import { Architecture } from 'aws-cdk-lib/aws-lambda';
const bunLayerX64 = LayerVersion.fromLayerVersionArn(
this, 'BunLayerX64',
'arn:aws:lambda:us-east-1:123:layer:bun-x64:1'
);
new Function(this, 'MyFunction', {
architecture: Architecture.X86_64,
layers: [bunLayerX64], // Must match
});
Production-Ready Implementation Patterns
Pattern 1: Deno with HTTP Server + Lambda Adapter
Here’s what works well for API workloads:
// main.ts - Deno with oak framework
import { Application } from "https://deno.land/x/[email protected]/mod.ts";
const app = new Application();
app.use((ctx) => {
ctx.response.body = { message: "Hello from Deno on Lambda!" };
});
const port = parseInt(Deno.env.get("PORT") || "8080");
console.log(`Server running on port ${port}`);
await app.listen({ port });
# Optimized Dockerfile
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter
FROM denoland/deno:bin-2.6.3 AS deno-bin
FROM debian:bookworm-slim
# Minimal dependencies
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
# Copy binaries
COPY --from=deno-bin /deno /usr/local/bin/deno
COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /var/task
ENV DENO_DIR=/var/deno_dir PORT=8080
# Application
COPY . .
# Pre-warm cache (critical optimization)
RUN timeout 10s deno run -A main.ts || [ $? -eq 124 ] || exit 1
CMD ["deno", "run", "-A", "main.ts"]
Cache pre-warming is what holds Deno container initialization at the low end of the range reported above; the benchmark image in Butz’s stack uses the same step. The Lambda adapter leaves the handler as a plain HTTP server, so the same code runs locally without a Lambda shim, and TypeScript needs no build step.
Pattern 2: Hybrid Approach - Runtime per Workload
Use the runtime that fits each function type. The selection collapses to four cases:
| Function profile | Runtime |
|---|---|
| Cold start sensitive and AWS SDK heavy | Node.js (managed) |
| CPU-bound with steady warm traffic | Node.js (managed) until a benchmark favours a Bun container |
| Background work, TypeScript-first, predictable traffic | Deno (container) |
| Everything else | Node.js (managed) |
That second row stays open until you measure it. On the only CPU-bound workload measured above, at 128 MB, Bun’s warm invocations were the slowest of the three. Bun takes the slot once a benchmark on your own workload and memory size puts it ahead.
Architecture example:
- API Gateway endpoints: Node.js (I/O-bound, cold start sensitive)
- Image processing: Node.js for now, with a Bun container benchmarked at the target memory size (CPU-intensive, high memory)
- Scheduled tasks: Deno container (TypeScript-native, predictable traffic)
Alternative Approaches to Consider
Optimize Node.js First
Before switching runtimes, consider Node.js optimizations:
// Bad: initialization in handler
export async function handler(event: APIGatewayEvent) {
const db = await createDatabaseConnection(); // Cold start penalty
// ...
}
// Good: initialization at module level
const db = await createDatabaseConnection(); // Outside handler
export async function handler(event: APIGatewayEvent) {
// Use pre-initialized db
}
ES Modules for tree shaking:
// Old: CommonJS imports entire module
const AWS = require('aws-sdk');
// New: ES Modules import only needed code
import { S3Client } from '@aws-sdk/client-s3';
These changes often deliver similar gains without the complexity of a runtime switch.
Evaluate Rust or Go for Maximum Performance
For CPU-bound workloads, compiled languages outperform every JavaScript runtime, and Lambda runs them through the same provided.al2023 custom runtime interface described above.
Trade-offs:
- Faster execution and lower memory use per invocation
- A different language means a skills investment for the team
- Longer compilation times, less flexible for rapid iteration
Choosing the Runtime
The managed Node.js runtime holds as the default for I/O-bound functions behind API Gateway, for bursty traffic, and for code that leans on the AWS SDK. AWS tunes initialization for its own runtimes and applies security updates to them without your involvement. Container images move that work back to you: AWS states that you are responsible for rebuilding the image from the latest base image and redeploying, and that deprecation notifications are not available for functions packaged that way. The surrounding tooling assumes Node.js as well.
Override that default when a function is CPU-bound, runs on traffic steady enough to keep execution environments warm, and has a cold start budget you have measured. Container images are the stronger option there, because they allow cache pre-warming. The price is base image patching, longer deployments, and ECR storage. Layers deploy faster and can be shared across functions. They force you to match architectures and count against the 250 MB unzipped limit AWS sets for a deployment package including layers and custom runtimes. Bun is the harder sell today, and packaging may account for part of that. The layer path carried the highest initialization cost in the figures above, while containerised Bun sits in the same band as the others. Since no published benchmark isolates packaging, treat that ordering as a hypothesis to test on your own function. Either way there is less production troubleshooting material to lean on.
Before switching either way, exhaust the Node.js options: module-level initialization, ES module imports, and provisioned concurrency. None of them add operational surface. If a proof of concept still favours an alternative runtime, keep it on one non-critical function and compare initialization duration and cold start rate against the Node.js baseline. Test locally with the Lambda Runtime Interface Emulator before trusting any cold start number.
References
- Building a Custom Runtime for AWS Lambda - Official guide to the bootstrap executable interface and runtime lifecycle
- Using the Lambda Runtime API for Custom Runtimes - HTTP API endpoints for invocation, response, and error handling
- Lambda Runtimes - Supported managed runtimes, deprecation schedules, and the split of patching responsibility between AWS and container-image owners
- AWS Lambda Quotas - The 250 MB unzipped deployment package limit, the 128 MB to 10,240 MB memory range, and the note that CPU is allocated in proportion to memory with one vCPU at 1,769 MB
- AWS Lambda Execution Environment Lifecycle - INIT, INVOKE, and SHUTDOWN phases, and the statement that cold starts typically occur in under 1% of invocations
- AWS Lambda Pricing - The per-GB-second duration tiers for x86 and arm64 and the per-request rate used in the cost section
- AWS Lambda Standardizes Billing for the INIT Phase - Why initialization time appears on the bill for every configuration from 1 August 2025, and which packaging types already paid for it
- JavaScript Lambda Runtime Benchmarking - Jason Butz’s May 2025 comparison of Node.js, Deno, and Bun on Lambda; the source of the 128 MB initialization and invocation figures above
- poc-aws-lambda-benchmark - The CDK stack behind that benchmark, showing the region, memory size, architecture, packaging, and SHA3-512 workload
- Benchmarking AWS Lambda Cold Starts Across JavaScript Runtimes - Deno’s own July 2024 study, with all three runtimes containerised at 512 MB; the source of the per-framework initialization figures
- serverless-coldstart-benchmarks - Raw data and harness for that study, confirming it reads CloudWatch initialization duration and forces a cold start per iteration
- Bun Documentation - Official Bun runtime documentation covering APIs, bundler, and test runner
- bun-lambda Package - Official Bun Lambda Layer, including the
--archflag values and thefetchhandler shape - AWS Lambda Web Adapter - Runs web servers (Deno, Bun, and others) on Lambda from container images; documents
AWS_LWA_PORTand its 8080 default - Lambda Runtime Interface Emulator - Local proxy for testing container images against the Runtime API before deploying
Related posts
When a Lambda fleet outgrows Middy's static middleware model, how a project-specific engine handles per-request config, and what owning one costs
A measured benchmark of 9 bundlers and 3 cdk synth runners for CDK TypeScript Lambdas, with a per-layer default and the rule that picks each one.
Match architecture weight to each runtime's init-amortization: lean handlers on single-purpose Lambda, more on a Lambdalith, full OOP/DI only on long-lived runtimes.
How to slice AWS Lambda functions: default to single-purpose, treat the single-domain Lambdalith as an earned exception, and the platform forces that decide it.
DI containers, monolithic SDKs, god-handlers, top-level secret fetches, and heavy ORMs: what they cost on cold start, and the functional shape that replaces them.