Elastic Beanstalk is the oldest of AWS's "give us your code and we run it" services. You upload a source bundle, choose a platform such as Python, Node.js, Java with Corretto or Docker, and Beanstalk provisions the load balancer, Auto Scaling group, EC2 instances, security groups and CloudWatch alarms needed to run it. Unlike a fully managed container service, all of those resources live in your account, visible and billable, and you can reach into any of them. That is both its strength and the source of most of its surprises.
This article explains Beanstalk from the inside: the object model, what an environment really is, the exact order in which an instance installs your code, how each deployment policy moves traffic, how worker environments turn SQS messages into HTTP requests, and how health and platform updates work. It ends with failure modes, trade-offs against newer services and a checklist. For the container-native alternatives, see AWS App Runner and Amazon ECS in depth. Option names and defaults here were checked against the Elastic Beanstalk developer guide as published on 2026-10-01.
The object model: application, version, environment
Beanstalk has a small vocabulary, and getting it right prevents most confusion. An application is a named folder. An application version is an immutable, labelled pointer to a source bundle (a zip, or a Docker definition) stored in an S3 bucket that Beanstalk creates per Region and account. An environment is a running copy of exactly one version on one platform version, with a configuration. Several environments, such as staging and production, can run different versions of the same application.
The platform is the AMI plus the language runtime, the nginx reverse proxy and the Beanstalk platform engine that performs deployments. Platforms are organised into platform branches, such as a particular runtime on Amazon Linux 2023, and each branch receives new platform versions for patches. Amazon Linux 2 reached end of life on 30 June 2026, and every AL2-based Beanstalk branch had a retirement date no later than that. If you still run one, migrating to an AL2023 branch is overdue rather than upcoming.
An environment also has a tier: a web server environment sits behind a load balancer and serves HTTP, while a worker environment consumes an SQS queue. Underneath, every environment is a CloudFormation stack that Beanstalk owns. Beanstalk is a control plane that generates and updates that stack from your configuration, then drives the instances through deployments.
Configuration and its precedence
Every setting is an option in a namespace, for example aws:autoscaling:asg for instance counts or aws:elasticbeanstalk:command for deployments. Options can come from four places, and when the same option is set in several, the highest wins: settings applied directly to the environment through the console, CLI or API; then saved configurations; then .ebextensions configuration files in the source bundle; then defaults.
This ordering explains a classic surprise: a value in a configuration file appears to be ignored because the console or the EB CLI set it directly when the environment was created. The developer guide notes that the EB CLI and console apply recommended values for deployment options, which you must remove if you want configuration files to control them. Keep configuration in one place, preferably the source bundle, and audit what is set directly with aws elasticbeanstalk describe-configuration-settings.
# .ebextensions/01-options.config (YAML, applied on every deployment)
option_settings:
aws:autoscaling:asg:
MinSize: 2
MaxSize: 8
aws:elasticbeanstalk:command:
DeploymentPolicy: Immutable
Timeout: "900"
aws:elasticbeanstalk:application:environment:
APP_ENV: production
LOG_LEVEL: info
aws:elasticbeanstalk:environment:process:default:
HealthCheckPath: /healthz
What happens on an instance during a deployment
When a new version reaches an instance, the platform engine runs a fixed sequence. Knowing it tells you where to put every customisation, and why something you placed in the wrong step silently does nothing. On Amazon Linux 2 and 2023 platforms the hooks documentation implies this order:
- Download and extract the source bundle into a staging directory.
- Run the
commandssection of any.ebextensionsfile. These run before the application is set up and are suited to machine-level preparation. - Run executables in
.platform/hooks/prebuild, then anyBuildfilecommands. - Set up the application and web server: install dependencies, apply proxy configuration from
.platform/nginx. - Run
container_commandsfrom.ebextensions, which execute in the staging directory and can useleader_onlyso that one instance runs, for example, a database migration. - Run
.platform/hooks/predeploy, then start processes from theProcfile. - Move the application to its final location, start the proxy, and run
.platform/hooks/postdeployas the last step.
Hook files run as root in lexicographic order, and a non-zero exit aborts the deployment, so name them 01_, 02_ and keep them idempotent. Hooks can read environment properties, and the get-config utility on the instance returns configuration values. A configuration-only change, such as editing an environment property, does not run the application hooks; it runs the parallel set under .platform/confighooks. If your hook writes a file derived from an environment property, put it in both places.
my-app/
Procfile # web: gunicorn app:app --bind 0.0.0.0:8000
.ebextensions/01-options.config
.platform/
nginx/conf.d/client_max_body.conf # client_max_body_size 20M;
hooks/predeploy/01_render_config.sh
confighooks/predeploy/01_render_config.sh
app.py
requirements.txt
#!/bin/bash
# .platform/hooks/predeploy/01_render_config.sh
set -euo pipefail
LEVEL=$(/opt/elasticbeanstalk/bin/get-config environment -k LOG_LEVEL)
echo "level=${LEVEL}" > /var/app/staging/runtime.conf
Deployment policies, and how each moves traffic
The policy decides how a new version replaces the old one across the fleet. It is set with DeploymentPolicy in aws:elasticbeanstalk:command; rolling policies add BatchSizeType (Percentage or Fixed) and BatchSize.
| Policy (DeploymentPolicy value) | What happens | Capacity during deploy | Rollback |
|---|---|---|---|
| All at once (AllAtOnce) | New version deployed to every instance simultaneously | Short outage on all instances | Redeploy the previous version |
| Rolling (Rolling) | Batches are detached from the load balancer, updated and reattached | Reduced by one batch | Redeploy; completed batches keep the new version |
| Rolling with additional batch (RollingWithAdditionalBatch) | An extra batch is launched first, then rolling proceeds | Full | Redeploy |
| Immutable (Immutable) | A full new set of instances in a separate Auto Scaling group | Full, temporarily doubled | Terminate the new group; old instances untouched |
| Traffic splitting (TrafficSplitting) | Like immutable, plus a percentage of traffic to the new set for an evaluation period | Full, temporarily doubled | Traffic moves back; new instances terminated |
Rolling deployments detach a batch from the load balancer, deploy, reattach and wait for health before moving on. With enhanced health, the developer guide states that instances must pass 12 consecutive health checks with status OK within two minutes for web environments. If a batch is not healthy within the command timeout the deployment fails, and here is the trap: batches that already finished keep the new version while the rest keep the old one. Beanstalk does not roll the finished batches back. You recover by deploying a known-good version.
Immutable and traffic-splitting deployments avoid the mixed fleet by launching a complete new set of instances in a temporary Auto Scaling group. Traffic splitting needs an Application Load Balancer and sends a configured percentage, NewVersionPercent, to the new instances for EvaluationTime minutes before shifting everything. Both double the instance count for the duration, so check your EC2 quotas, subnet IP space and any per-instance licence counts. Both also discard accumulated burst credits on T-family instances.
The fifth option is outside the policy list: blue/green by CNAME swap. You create a second environment with the new version, test it at its own URL, and swap the environment CNAMEs. Cutover is a DNS change, so clients and resolvers that cache the old answer keep hitting the old environment until their TTL expires; keep blue running until traffic drains. Blue/green is the only option that lets you change something an in-place deployment cannot, such as the platform branch or the load balancer type.
Worked example: shipping a Flask API with a worker
Consider a small team with a Flask API that also needs to generate PDF reports, which take ten to thirty seconds each. Generating them inside a web request would tie up workers and time out at the load balancer. The Beanstalk-native shape is a web environment and a worker environment from the same codebase.
pip install awsebcli
eb init reports-api --platform "Python 3.12" --region eu-west-1
eb create reports-web --elb-type application --instance_type t3.small
eb create reports-worker --tier worker --instance_type c7g.large
eb deploy reports-web # creates an application version and rolls it out
eb health reports-web # per-instance enhanced health, live
eb logs reports-web --all # bundles instance logs from S3The web app sends a message to the worker's queue and returns 202 with a job ID. On each worker instance, the Beanstalk SQS daemon, aws-sqsd, reads the queue and POSTs each message body to http://localhost/ on port 80, or to the HTTP path you configure. A 200 OK response makes the daemon delete the message; any other status returns the message to the queue after the error visibility timeout, and no response returns it after the inactivity timeout, which defaults to 180 seconds.
# worker app.py
from flask import Flask, request
app = Flask(__name__)
@app.post("/")
def handle():
job = request.get_json()
msg_id = request.headers.get("X-Aws-Sqsd-Msgid")
if already_done(job["job_id"]): # SQS is at-least-once: be idempotent
return "", 200
render_pdf(job) # 10-30 s
mark_done(job["job_id"], msg_id)
return "", 200
# cron.yaml at the bundle root: periodic tasks enqueue a message on schedule
version: 1
cron:
- name: "nightly-cleanup"
url: "/cleanup"
schedule: "0 2 * * *"Three settings decide whether this works. The visibility timeout must be longer than the slowest job, or a second instance picks up the message while the first is still rendering. HTTP connections, default 50, is the per-instance concurrency; for CPU-bound PDF rendering set it near the vCPU count. Max retries, default 10, governs when a poisoned message moves to the dead-letter queue that an autogenerated queue gets by default. Periodic tasks are enqueued by a leader instance chosen through a DynamoDB table, so exactly one instance schedules, but any instance may execute. Periodic tasks are not supported if you attach an existing FIFO queue.
Health, logs and platform updates
Basic health is just the load balancer's view. Enhanced health adds an agent on each instance that reports request rates, latency percentiles, 5xx ratios, load and deployment status, and rolls them into colours: Ok, Warning, Degraded and Severe. Use HealthCheckPath pointing at an endpoint that checks the process is serving, not one that queries every dependency. A health check that fails whenever the database is slow will make Beanstalk replace healthy instances, turning a database incident into a capacity incident.
Turn on log streaming to CloudWatch Logs with a retention period; on-demand log bundles only cover instances that still exist.
Managed platform updates apply new patch or minor platform versions in a weekly maintenance window that you choose, using an immutable-style replacement. Enable them for production: the alternative is manual updates that are easy to postpone for a year. Major moves, such as a new runtime major version or AL2 to AL2023, are not automatic; do them as blue/green with a clone of the environment.
On 17 September 2026 AWS announced Cluster Mode, which runs multiple Beanstalk applications on shared EKS-powered infrastructure instead of one environment per application. It is new; do not assume the EC2 behaviour described here carries over.
Failure modes
- Out-of-band edits. Changing the Auto Scaling group, security groups or load balancer directly in their consoles creates drift that the next environment update may overwrite or fail on. Make changes through Beanstalk options or
.ebextensionsresources. - Database inside the environment. An RDS instance created by the environment is deleted with it. Create the database separately and pass its endpoint and secret reference as configuration, as the RDS article describes.
- Half-finished rolling deployment. A mixed fleet serves two versions; schema changes must therefore be backward compatible for at least one release.
- Long command timeouts hiding failures. A hook that hangs blocks the batch until the timeout; add timeouts inside scripts.
- Local state. Uploaded files written to instance disk disappear on replacement, which immutable deployments and platform updates perform routinely. Use S3 or EFS.
- Version label sprawl. Each deploy keeps a bundle in S3 and counts against the application version quota; configure a version lifecycle policy.
Trade-offs: when Beanstalk is the right tool
Beanstalk fits teams that want a conventional EC2 deployment, with instances they can SSH into, OS-level packages and long-running processes, without writing the CloudFormation and deployment automation themselves. It is a good home for monoliths and for workloads that need instance features a container service hides.
| Need | Beanstalk | Alternative |
|---|---|---|
| Per-instance customisation, agents, OS packages | Natural: hooks and ebextensions | ECS on EC2 with custom AMIs |
| Fast deploys, many small services | Slow: minutes per deploy, one stack per environment | ECS or App Runner |
| Scale to near zero | Minimum one instance per environment | Lambda or App Runner |
| Full infrastructure as code | Possible, but Beanstalk owns the stack | CDK with explicit resources |
The cost is control: Beanstalk decides resource names, stack shape and deployment mechanics, and its deployments are slower than container rollouts because instances must run the full workflow. Scaling itself is ordinary EC2 Auto Scaling, explained in Auto Scaling groups.
What to do next
- List your environments and their platform branches; move anything on an Amazon Linux 2 branch to AL2023 with a cloned environment and a CNAME swap.
- Move every option you rely on into
.ebextensionsand remove conflicting directly-applied settings so the source bundle is the source of truth. - Choose Immutable or traffic splitting for production, and confirm quotas and subnet space can absorb a doubled fleet.
- Put each customisation in the correct workflow step, mirror config-derived hooks into
confighooks, and make every hook idempotent. - Point
HealthCheckPathat a cheap liveness endpoint and enable enhanced health, log streaming and managed platform updates. - For workers, set the visibility timeout above your slowest job, size HTTP connections to the instance, and make handlers idempotent.
- Keep databases and uploaded files outside the environment.