I tried running GitHub Actions self-hosted runners on Lambda MicroVMs
This page has been translated by machine translation. View original
This is Iwata from the Retail App Co-Creation Department @ Osaka.
After reading the following article that was recently published, I became interested in trying out a configuration for building GitHub Actions self-hosted runners on Lambda MicroVMs.
My thinking was whether using Lambda MicroVMs could resolve the issues of startup time and scale-out speed that are cited as disadvantages of the architecture where Runners are launched on ECS each time.
So I was about to set up a verification environment right away...
and I found that someone had already published their work on Construct Hub. This time, I'll use this Construct to build self-hosted runners on Lambda MicroVMs and verify the setup.
Environment
The environment used this time is as follows.
- cdk-github-microvm-runners: 0.1.7
- aws-cdk-lib: 2.266.0
Architecture Overview
The details are explained in the cdk-github-microvm-runners GitHub repository.
The rough flow is as follows.
- When a GitHub Actions workflow starts, it calls Lambda via WebHook
- Lambda validates the caller and, if there are no issues, puts a message into SQS
- Lambda is triggered from SQS, Lambda starts a MicroVM and registers a self-hosted runner with GitHub as a Just-in-time runner
- The self-hosted runner executes the workflow
The information of the started MicroVM is managed in DynamoDB, and it is controlled to prevent duplicate job executions.
Let's Try It
Now let's get started and build the self-hosted runner on Lambda MicroVMs environment.
Deploy all necessary resources with CDK
First, run npx cdk init --language typescript to prepare the CDK code, then install cdk-github-microvm-runners with npm install cdk-github-microvm-runners.
Once ready, write the CDK code following the Getting started guide.
import { App, CfnOutput, Stack } from 'aws-cdk-lib';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import {
GithubAppId,
GithubAppKey,
GithubAuth,
GithubMicrovmRunners,
MicrovmSize,
RunnerScope,
} from 'cdk-github-microvm-runners';
const app = new App();
const stack = new Stack(app, 'Runners', { env: { region: 'us-east-1' } });
const appId = Secret.fromSecretNameV2(
stack,
'AppId',
'microvm-runner/dev/app-id',
);
const privateKey = Secret.fromSecretNameV2(
stack,
'AppKey',
'microvm-runner/dev/app-private-key',
);
const webhookSecret = Secret.fromSecretNameV2(
stack,
'WebhookSecret',
'microvm-runner/dev/webhook-secret',
);
const runners = new GithubMicrovmRunners(stack, 'Runners', {
github: GithubAuth.app({
appId: GithubAppId.fromSecret(appId),
privateKey: GithubAppKey.fromSecret(privateKey),
webhookSecret,
}),
scope: RunnerScope.org('<Set the name of the Org where you want to use the self-hosted runner here>'),
});
// A runner class: the `microvm` label, on 4 GB VMs.
runners.addRunnerClass('microvm', { size: MicrovmSize.GB4 });
new CfnOutput(stack, 'WebhookUrl', { value: runners.webhookUrl });
new CfnOutput(stack, 'SetupCommand', { value: runners.setupCommand });
The key point is to set the GitHub Org name where you want to register the self-hosted runner in RunnerScope.org.
Now that the code is ready, deploy the stack with npm cdk deploy. After waiting a while, the various resources will be created.

The MicroVM image is also registered.

By the way, when I downloaded the Dockerfile used to build the MicroVM image above from S3, the contents were as follows.
FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y libicu bash git docker jq tar zip unzip nodejs22 sudo shadow-utils && dnf clean all
RUN dnf install -y gh --repofrompath gh-cli,https://cli.github.com/packages/rpm || true
RUN curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip -o /tmp/awscliv2.zip && unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install && rm -rf /tmp/aws /tmp/awscliv2.zip
RUN useradd -m runner && usermod -aG docker runner || groupadd docker && usermod -aG docker runner
RUN mkdir -p /opt/runner && cd /opt/runner && curl -fsSLo r.tgz https://github.com/actions/runner/releases/download/v2.335.1/actions-runner-linux-arm64-2.335.1.tar.gz && tar xzf r.tgz && rm r.tgz && chown -R runner:runner /opt/runner
COPY microvm-runner/agent.mjs /opt/microvm-runner/agent.mjs
COPY microvm-runner/entrypoint.sh /opt/microvm-runner/entrypoint.sh
RUN chmod +x /opt/microvm-runner/entrypoint.sh
ENTRYPOINT ["/opt/microvm-runner/entrypoint.sh"]
If you want to customize the image, you can also adjust it in the CDK code. For example, there is a way to write it using RunnerImage.fromInline like this:
runners.addRunnerClass('custom', {
size: MicrovmSize.GB4,
image: RunnerImage.fromInline(`
FROM public.ecr.aws/lambda/microvms:al2023-minimal
...
omitted
`),
});
For more details, see the following link.
Setting Up the GitHub App
Now that the stack is deployed, let's create the GitHub App. The setup command is also output in the CFn stack Output we deployed earlier, and it looks like this:
npx cdk-github-microvm-runners@0.1 setup --org <target GitHub Org name> --stack Runners --region us-east-1
For the --stack option, use the CFn stack name, and for --region, use the region where the stack was deployed.
When you run the above command, a screen to create a GitHub App opens, so specify a unique name.

You are asked to confirm whether to install the created GitHub App to the Org, so click Install.
Once the installation is complete, you can close the browser.
This completes all the necessary preparations. Behind the scenes, the secret values such as microvm-runner/dev/app-id specified in the CDK code have also been registered.

Running a Workflow That Uses the Self-Hosted Runner
Now that everything is ready to use the self-hosted runner, let's create a repository and trigger a GitHub Actions workflow. First, I tried the following simple workflow.
on:
workflow_dispatch
jobs:
test:
runs-on: [self-hosted, microvm]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: test
run: |
echo test
sleep 120
After triggering the workflow, checking Runners from the Org settings shows that the self-hosted runner has been registered.

Waiting a few more seconds, the workflow starts executing on the registered self-hosted runner.
The logs show the following. You can see it is running correctly.

Looking at the log details, it seems it takes about 20 seconds for the job to be picked up by the runner.
2026-08-27T02:56:56.9760000Z Waiting for a runner to pick up this job...
2026-08-27T02:57:15.4380000Z Job is about to start running on the runner: microvm-runner-ba2f8119-bc1605de
According to the cdk-github-microvm-runners documentation, the time required for each phase is as follows. The total wait time from when the workflow is triggered to when processing actually begins is about 25 seconds.
| Segment | Time | What happens |
|---|---|---|
| Queued → the VM is running | 6.8 s | webhook delivery, the queue, the launcher, VM boot |
| VM running → the runner's first log line | 8.2 s | run.sh, the .NET host starting, assemblies loading |
| Runner starting → connected to GitHub | 9.0 s | reading its configuration, registering |
| Connected → the job begins | 1.0 s | GitHub assigns the queued job to it |
| Total | 25 s |
※ Table quoted from https://github.com/schuettc/cdk-github-microvm-runners/blob/main/docs/architecture.md
While we're at it, let's also check the contents of the DynamoDB table. Scanning the table showed that the following items were registered.

Looking at the actual data makes it easier to visualize the processing happening behind the scenes.
Let's take the opportunity to create another workflow and verify that Docker can be used. The workflow definition is as follows.
on:
workflow_dispatch
jobs:
test:
runs-on: [self-hosted, microvm]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: test
run: |
docker run --rm hello-world
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
The run section executes the docker command, and the services specification starts a postgres container.
The logs when this workflow was executed were as follows.


Docker is indeed available on Lambda MicroVMs. When you select Lambda as the execution environment for CodeBuild, Docker cannot be used, but with self-hosted runners on Lambda MicroVMs, Docker works without any issues.
Discussion
I was able to run GitHub Actions workflows on self-hosted runners on Lambda MicroVMs, but let me consider whether this architecture is actually an effective one.
Comparing costs assuming the same processing time
First, let me do a simple cost comparison under the assumption that the workflow execution time does not change when using self-hosted runners.
For GitHub-hosted runners, the SKU linux_2_core_arm costs $0.005/minute. If you launch the equivalent of linux_2_core_arm on Lambda MicroVMs, even in the Virginia region the calculation is as follows.
- vCPU price per second: 2vCPU × $0.0000276944
- Memory price per second (per GB): 8GB × $0.0000036667
The cost per minute is (2×0.0000276944 + 8 * 0.0000036667) × 60 = $0.00508334. For simplicity I've omitted it, but in practice there are also additional storage costs for saving snapshots, etc. Therefore, in a simple calculation, GitHub-hosted runners come out cheaper, but there is a possibility of gaining a cost benefit by utilizing Saving Plans and similar options.
Also, with Lambda MicroVMs, billing is per second rather than per minute, which can be considered an advantage. Since GitHub-hosted runners bill rounded up to the minute, there may be cases where Lambda MicroVMs would bill for 1 minute and 31 seconds while GitHub-hosted runners would bill for 2 full minutes.
Will the processing time actually be the same?
The earlier calculation was based on the assumption that the workflow execution time does not change, but what is the reality?
The cdk-github-microvm-runners used this time is designed to register self-hosted runners as Just-in-time runners and Terminate the MicroVM each time, but Lambda MicroVMs support pause and resume. If you re-implement the architecture so that the MicroVM is paused after workflow execution completes and the paused MicroVM is resumed the next time the same workflow starts, you could maximize the Lambda MicroVMs characteristic of being able to retain state.
For example, suppose a workflow includes a step that runs apt install. On the first MicroVM startup, the package installation process runs in full, making it time-consuming, but from the second workflow execution onward, if the MicroVM with the packages already installed is resumed, the installation process would be skipped and the command execution should complete quickly.
Taking it further, you could also consider an approach of building a MicroVM image optimized for your own workload. Rather than running apt install in the workflow, if you run apt install during the MicroVM image build, you can further reduce workflow processing time.
Whether you want to use self-hosted runners only for a specific project, or provide self-hosted runners to multiple teams as an organizational foundation — the approach to how much you can customize the MicroVM image will vary depending on requirements, but reducing workflow processing time by leveraging the characteristics of self-hosted runners seems worth considering.
Summary
I built and tested GitHub Actions self-hosted runners on Lambda MicroVMs. With some more work, it seems like you could do some really interesting things with this setup, doesn't it?
If you're struggling with GitHub Actions processing time or costs, why not consider the self-hosted runner on Lambda MicroVMs architecture?