Fix AWS EC2 User-Data Script Did Not Run: Cloud-Init Logs to Read
If your user data script did not run on an EC2 Linux instance, read /var/log/cloud-init-output.log first — it holds every line the script printed. On Windows, read C:\ProgramData\Amazon\EC2Launch\log\agent.log instead. But here is the part almost nobody checks first: user data only runs once, on an instance's very first boot, by default — so if you stopped, started, or rebooted that instance after editing the script, nothing is broken at all. It simply never ran a second time, because it was never told to.
Jake called on a Tuesday, half-annoyed and half-panicked. He'd launched a fresh EC2 instance for a customer's point-of-sale backend, pasted in a user data script to install the software and pull a config file, and walked away to grab coffee. Twenty minutes later: no software, no config file, nothing. The customer was due to pick the machine up at 5pm, and Jake had already told them it would be ready.
"I checked the instance," he said. "It's running. It's green. It just... didn't do the thing I told it to do." That sentence — "it just didn't do the thing" — is the single most common complaint about EC2 user data, and it covers at least four completely different problems that all look identical from the outside. Sorting out which one you actually have is most of the fix.
The Linux log: cloud-init, and where it writes
On almost every Linux AMI you'll launch on EC2 — Amazon Linux, Ubuntu, Debian, RHEL, SUSE — the program that reads your user data and acts on it is called cloud-init. Think of cloud-init as the instance's onboarding checklist: the moment the machine boots for the first time, cloud-init reaches out to a special internal address, fetches whatever you typed into the "user data" box when you launched the instance, figures out what kind of thing you gave it, and runs it. You never see any of this happen, because it all takes place before you'd normally be able to log in.
cloud-init keeps two logs, and they answer two different questions:
| Log file | What it tells you | Read it when |
|---|---|---|
/var/log/cloud-init-output.log |
The standard output and error text your script actually printed — the same thing you'd have seen scrolling past if you'd typed the commands yourself over SSH. | First. This is where "command not found" and "package X has no installation candidate" show up. |
/var/log/cloud-init.log |
cloud-init's own internal diary — which modules it ran, in what order, whether it recognized your user data as a script at all, and any error it hit before your script even started. | When cloud-init-output.log is empty or missing — that almost always means cloud-init never got as far as running your script, and this file says why. |
To view them, connect over SSH the normal way and run:
sudo cat /var/log/cloud-init-output.log— scroll to the bottom first; that's where the most recent, most relevant output sits.- If that file is empty, thin, or doesn't mention your script at all, run
sudo cat /var/log/cloud-init.logand search it for the worderrororWARN. - If you want to isolate just your own commands from the framework noise,
sudo grep -A 50 "user-data" /var/log/cloud-init.lognarrows the search.
♂️ Jake's Reality Check
"I logged in, ran cat on that file, and it's basically empty. Two lines. Did I break cloud-init?"
No — an empty or two-line cloud-init-output.log almost always means cloud-init never handed your script anything to run. It's not damaged; it just never had a job. That points you at the next section, not at reinstalling anything.
The Windows log: EC2Launch, not cloud-init
None of the above applies on Windows. cloud-init doesn't run there at all — instead, the AMI ships with an agent called EC2Launch (or, on older AMIs, its predecessor EC2Config). It does the same job — read the user data, decide what it is, run it — but it keeps its own logs in its own place.
- EC2Launch v2 (the current agent, shipped on newer Windows Server AMIs):
C:\ProgramData\Amazon\EC2Launch\log\agent.log - EC2Launch (v1) (older AMIs):
C:\ProgramData\Amazon\EC2-Windows\Launch\Log\UserdataExecution.log - EC2Config (legacy, pre-2016-ish AMIs, rare to still see): logs sit under
C:\Program Files\Amazon\Ec2ConfigService\Logs\
To open the log, connect over RDP and either double-click the file in File Explorer, or open PowerShell and run Get-Content -Tail 100 "C:\ProgramData\Amazon\EC2Launch\log\agent.log" to see the most recent activity without scrolling through the whole thing. That log captures the standard output and standard error streams from your script — exactly what would have printed if you'd typed the same commands into a Command Prompt window yourself.
What changed between versions
- Before: the original EC2Config service handled user data on older Windows Server AMIs, logging to its own Ec2ConfigService folder.
- Now: current Windows Server AMIs ship EC2Launch v2, which folds user data execution, log output, and drive initialization into one agent logging to
agent.log. - What that means for you: check which agent your AMI actually uses before you go hunting for a log file — an old AMI built years ago may still be running EC2Config or EC2Launch v1, and the folder names are completely different.
One Windows-specific trap worth knowing before you go further: if you're passing user data through the EC2 API or a tool that doesn't automatically base64-encode it for you, and the encoding is wrong, EC2Launch logs an error saying it couldn't find <script> or <powershell> tags to run. That's not a permissions problem or a syntax typo in your commands — it's the agent telling you it couldn't even parse what you sent it as a script at all. Windows user data must be wrapped in <script>...</script> (for batch/cmd) or <powershell>...</powershell> tags; plain commands with no tags are one of the most common reasons a Windows user data box "does nothing."
Which of these is actually happening to you?
"It didn't run" covers a handful of genuinely different situations, and the log tells you which one you're in almost immediately. Work through these in order.
1. It ran once already, and this isn't the first boot
By default, user data on EC2 runs exactly once: during the very first boot cycle after you launch the instance. If you stopped the instance, edited the user data, started it back up, and expected the new script to fire — it won't, unless you specifically configured it to run on every boot. This is, by a wide margin, the most common cause of "my script didn't run" reports, and it's the one that looks most like a real bug because nothing in the console tells you it happened.
2. It ran, and it failed partway through
This is the second most common case, and it's the one cloud-init-output.log or agent.log is built for. A command errored out, a package wasn't available yet because the network wasn't fully up, or a line assumed something about the environment that wasn't true. The log will show exactly where it stopped.
3. The instance never received it as a script at all
Wrong shebang line on Linux (a script must start with #! and the interpreter path, like #!/bin/bash, on its own first line), missing <script>/<powershell> tags on Windows, or a base64-encoding mismatch when you launched via the API instead of the console. cloud-init or EC2Launch didn't recognize the payload as something to execute, so it quietly skipped it.
4. There was no user data at all
Sounds obvious, but it happens constantly with Auto Scaling groups, launch templates, and infrastructure-as-code: the field was left blank in the template that actually launched the instance, even though you're sure you typed something in some version of it.
⚠️ What this actually breaks
If you're troubleshooting a fleet launched from a launch template or Auto Scaling group, don't assume every instance got identical user data. A template updated after some instances already launched means older instances are running the old version — check the template's version history, not just what's live now.
Ethan's rule of thumb, the one he repeats to Jake every time this comes up: "Read the log before you touch the script. Guessing and relaunching is the slowest way to debug this — the log almost always tells you in the first ten lines which of the four things happened."
Confirm the instance actually received the script
Before you dig into logs, it's worth thirty seconds to confirm the instance actually has the user data you think it has — this rules out cause 3 and 4 from the section above in one step.
- From the console: select the instance, choose Actions > Instance settings > Edit user data. This shows exactly what the instance was launched with — not what you meant to type, what actually got saved.
- From inside the instance (Linux): the instance metadata service holds a copy of it. Fetch a session token first (current AMIs default to IMDSv2, which requires this step), then request the data:
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"), thencurl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/user-data. - From the AWS CLI, without connecting at all:
aws ec2 describe-instance-attribute --instance-id i-xxxxxxxx --attribute userDatareturns the base64-encoded user data as EC2 has it stored, which you can decode locally.
If any of these three comes back empty, you've found the whole problem already — nothing was ever going to run. Stop the instance, add the user data, and start it again. Remember: you can only edit user data while the instance is stopped, not while it's running, and stopping an instance backed by instance store (not EBS) loses any data on that volume, so back up first if that applies to you.
Can't SSH or RDP in? Read the console output instead
Sometimes the reason you can't confirm any of this is that the instance itself is unreachable — the script that was supposed to configure networking or open a security group port is the very script that failed. For that situation, EC2 gives you two tools that don't need SSH, RDP, or even a working network stack on the instance.
Get system log
In the console, select the instance and choose Actions > Monitor and troubleshoot > Get system log. On Linux instances this shows the exact console output the instance would display on a physical monitor — including, if you set it up right (see the next section), your user data output. On Windows instances, console output shows the last three system event log errors rather than a full boot log, which is far less useful, so lean on the EC2Launch log file for Windows troubleshooting instead. One limit worth knowing: only the most recent 64 KB of output is kept, and it's available for at least an hour after the last time the instance posted anything — so on an instance that's been running a while, "Get system log" may only show you recent activity, not the original boot.
Get instance screenshot
If the instance is frozen, crashed, or you just need a visual sanity check, Actions > Monitor and troubleshoot > Get instance screenshot captures a JPG of whatever's currently on the instance's virtual monitor — often enough to see a login prompt, a kernel panic, or a stuck boot screen. It works whether the instance is running or has just crashed, and there's no data transfer charge for it. It doesn't work on bare-metal instance types, Graviton-based instances, or instances using an NVIDIA GRID driver.
EC2 Serial Console
For Nitro-based instance types, you can also open a live serial console session directly from the EC2 console or CLI — this gives you a real-time, interactive view of boot messages, including through a reboot, without needing the instance's network stack to be working at all. It's the closest thing to plugging a monitor and keyboard straight into the machine.
Make your script's output show up in the console log
By default, cloud-init-output.log only exists on the instance's own disk — if you can't connect, you can't read it, and "Get system log" won't show your script's output unless you've told the script to send it there too. On Amazon Linux 1, Amazon Linux 2, and Amazon Linux 2023, add this as the very first line block of your script, before any of your real commands:
#!/bin/bash -xe
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
echo "Starting setup"
That one line does two things at once: it writes everything your script prints to /var/log/user-data.log for later reading over SSH, and it pushes a copy to /dev/console, which is what "Get system log" reads. So even if the instance never finishes booting far enough for SSH to work, you'll still see exactly where the script got to. On RHEL 7, 8, and 9, the same redirect trick applies, though the exact log shipping path differs slightly by release — check the current RHEL-specific guidance if you're troubleshooting one of those AMIs.
The -xe flags on that first line matter too: -x makes bash print every command before it runs it (so you see exactly which line failed, not just its output), and -e makes the whole script stop immediately on the first command that errors, instead of plowing on and leaving you guessing which of ten later failures was the real cause.
✅ Why this is the one to use
Add the exec > >(tee ...) line to every user data script you write from now on, even ones you're confident about. It costs nothing, and it's the difference between a two-minute log read and an afternoon of blind relaunching the next time something goes wrong.
Why the script itself fails, once it does run
Once the log confirms the script did start, the failures that show up inside it are almost always one of a handful of repeat offenders. User data runs as root (on Linux) or the local System/Administrator account (on Windows) in a minimal, non-interactive environment — nothing like the shell you get over SSH or RDP.
- Running as root, but assuming a normal user's environment. Paths like
~or$HOMEdon't point where you expect. Use absolute paths. - No
sudoneeded — and using it breaks things. On Linux, user data already runs as root. Addingsudoin front of commands is harmless most of the time but occasionally trips up scripts that check whether they're already root. - The network isn't fully ready yet. A package install or a curl to an external URL that fires before networking has finished coming up will fail with a connection error, not a permissions error — look for that distinction in the log.
- A command that expects to prompt for input.
apt-get installwithout-y, for instance, will hang waiting for a "yes" that never comes, because there's no interactive terminal attached. This looks like the script "just stopped" rather than erroring. - A typo in the shebang line.
#!/bin/bashwith a stray space, or a Windows line ending (CRLF) sneaking in from a script edited on Windows and pasted into the user data box, can make the very first line unreadable to the interpreter. - A dependency that assumes an earlier line succeeded. Without
set -e, a failed command doesn't stop the script — it keeps going, and the real failure is buried several lines above whatever error finally makes the log. - A reboot or package manager lock mid-script. Some package installs trigger a kernel update that wants a reboot, or another process (an automatic security update, on some distros) is already holding the package manager lock when your script tries to use it — the log shows a "could not get lock" style error rather than anything about your own commands.
| What the log shows | What it usually means |
|---|---|
| Empty file, no mention of your commands | User data wasn't recognized as a script (bad shebang, missing tags) or wasn't present at all |
| Output stops mid-way with no error text | A command is hanging on a prompt it will never receive — check for missing -y flags |
| "command not found" | Package not yet installed at that point in the script, or a typo in the command name |
| "Could not resolve host" / connection errors | A network call fired before the instance's networking was fully up |
| "Permission denied" | Writing to a path the running user genuinely can't touch, or a file mode issue on a mounted volume |
| "could not get lock" / dpkg or rpm database busy | Another process (often an automatic OS update) is holding the package manager lock at the same moment your script wants it |
♂️ Jake's Reality Check
"My script ran fine the first three times I tested it by pasting it into the terminal myself. Why does it fail as user data?"
Because you tested it as a logged-in user with a full environment, and user data runs with almost none of that. Ethan's answer, verbatim: "Testing user data by typing the commands into an SSH session isn't testing user data at all — it's testing a completely different environment that happens to share the same commands."
Getting it to run again — on this boot, or every boot
Once you've fixed the script itself, you still have to actually get it to run — and "stop, edit user data, start" alone won't do it, because of the once-per-instance default covered above.
Option 1: Rerun the existing script manually
On Linux, cloud-init keeps a copy of the script it last ran at /var/lib/cloud/instances/instance-id/ — you can re-execute it by hand over SSH once you've fixed whatever broke, without touching the launch configuration at all.
Option 2: Force it to run on every boot going forward
If you genuinely want the script to run on every restart — not just the first one — Linux user data needs to be reformatted as a MIME multi-part file with a cloud-init directive telling it to run on every boot, not just the initial one. This is a deliberate opt-in, not a toggle, because most people do not actually want their setup script re-running every time the machine restarts.
Option 3: Add the persist tag on Windows
On Windows, if you include the persist tag in your user data, EC2Launch will keep executing it on subsequent starts and reboots rather than only the initial launch. Without it, Windows user data behaves the same as Linux: once, on first boot, by default.
⚠️ What this actually breaks
Once-per-boot user data that installs software or creates users is not automatically safe to run twice. If you switch a script to run every boot, make sure every command in it is idempotent — that it does the same right thing whether it's the first run or the fiftieth — or you'll end up with duplicate cron entries, repeated package reinstalls, or a script that errors out on the second boot because something it tries to create already exists.
When the script calls AWS itself: IAM and instance profiles
If your script calls the AWS CLI or an SDK — pulling a file from S3, reading a parameter from Systems Manager Parameter Store, registering the instance somewhere — and it fails at exactly that point, the log will usually show an access-denied or credentials error rather than a generic command failure. The instance needs an instance profile attached that carries an IAM role with permission to do whatever the script is asking for. A script that works fine when you SSH in and run the AWS CLI commands manually — because your own IAM user has broader permissions — can fail as user data purely because the instance's own role is narrower than yours.
Check this by looking at the instance's IAM role in the console (under the instance's Security tab) and comparing its attached policies against what the script actually calls. This is a common trap when copying a working script from one account or environment to another: the script is identical, but the instance profile attached to the new instance is missing a permission the old one happened to have.
The 16 KB limit, and other silent-fail traps
User data has a hard size ceiling: 16 KB, measured before base64 encoding, and it's an EC2-level limit, not something cloud-init or EC2Launch enforces. If you paste in a script that exceeds it, the launch itself will typically fail with a size error rather than silently truncating your script — so this rarely explains a "ran but stopped partway" situation. But it does explain a surprising number of "I can't even launch the instance" tickets, especially once a bootstrap script grows to include large inline configuration blocks or embedded certificates.
If you're bumping up against the limit, the fix is almost always the same one professionals reach for anyway: keep the user data itself short — install one thing, fetch a real setup script from S3 or a private repository, and hand off to that. It also compresses well: user data supports gzip-compressed content, which is useful specifically because it buys you more room under that same 16 KB ceiling.
⚠️ What this actually breaks
User data is not encrypted and not access-controlled beyond "anyone who can reach the instance's metadata service, or anyone with permission to view the instance in your account, can read it." Never put a password, a private key, or a long-lived secret directly in the script text. Fetch secrets at boot time from Secrets Manager or Parameter Store instead, using the instance's IAM role.
Edge cases: launch templates, Auto Scaling, and Spot Instances
Everything above assumes one instance, launched once, by someone who typed user data directly into the console. Fleets managed by launch templates, Auto Scaling groups, or Spot capacity add a few extra wrinkles worth knowing before you assume the script itself is at fault.
Launch templates are versioned, and each version carries its own user data. If a launch template has been edited several times, an Auto Scaling group or a manual launch can be pointed at a specific version number, at "$Default", or at "$Latest" — and these are not automatically the same thing. Before you assume every instance in a fleet got the script you're currently looking at in the console, check which template version actually launched the instance you're troubleshooting, not just which version is newest.
Fixing the template doesn't fix instances that are already running. An Auto Scaling group reads user data at launch time only. Updating a launch template's user data, or triggering an instance refresh, changes what happens to new instances going forward — it does not retroactively run anything on instances that already booted with the old version. If you need the fix applied to a currently running fleet, you're back to running the script manually (or via Systems Manager Run Command, covered below) or replacing the instances through an instance refresh.
Older launch configurations behave the same way as templates but without version history. A launch configuration attaches one fixed, unversioned user data blob. If your organization has migrated some Auto Scaling groups to launch templates and left others on the older launch configuration model, confirm which one is actually attached to the group you're troubleshooting — editing a launch template that nothing is using is a quiet way to lose an afternoon.
Spot Instances run user data exactly the same way as On-Demand — but they can be interrupted mid-script. Amazon EC2 issues a Spot Instance interruption notice two minutes before it reclaims a Spot Instance (except with hibernation, which begins immediately with no two-minute window). That notice is delivered both as an EventBridge event and as an item in the instance's own metadata. A user data script that's still running when that notice fires simply stops where it is — the instance is gone before any later lines, including a final "success" message at the end of the script, ever execute. On a Spot fleet, "the script seems to half-run sometimes" is worth checking against interruption timing before you assume the script itself is unreliable.
♂️ Jake's Reality Check
"I run my test boxes on Spot to save money. Could that be why my install script sometimes only half-finishes?"
It's worth ruling out, yes. If the log stops abruptly, mid-command, with no error text at all — not "command not found," not "permission denied," just silence — a Spot interruption landing before the script finished is a real possibility, not just a theory. A script that genuinely errors almost always leaves some kind of message behind; one that simply vanishes mid-line more often means the instance itself vanished first.
The AMI gotcha: leftover scripts on golden images
Here's one that bites people building golden images: when cloud-init processes a script from user data, it copies that script to /var/lib/cloud/instances/instance-id/ and leaves it there — it isn't deleted after running. If you then create an AMI from that instance to reuse as a base image, the old script comes along for the ride, sitting on disk in every future instance launched from it. That's not automatically dangerous, but it can be confusing months later when someone finds an unfamiliar script on a "fresh" instance and can't work out where it came from, and it can leak information you didn't intend to bake into a shared image — including across accounts, if that AMI is ever shared. Delete the scripts from that instances directory before you snapshot an AMI from a bootstrapped instance.
Related, but the opposite direction: user data itself is stored as an instance attribute, not baked into the disk image, so creating an AMI from an instance does not carry its user data forward — each new instance launched from that AMI starts with no user data unless you explicitly give it some at launch. Don't assume a golden image "remembers" the script that built it; only the leftover copy on disk does, and only if you forgot to clean it up.
Ship these logs somewhere durable: the CloudWatch agent
Everything covered so far lives on the instance's own disk. That's fine for a single box you can still reach, but it falls apart the moment you terminate the instance mid-debug, or you're troubleshooting a fleet where connecting to each machine individually just to read one file doesn't scale. The fix is to have the instance ship its own boot log to a central place automatically.
AWS's current recommendation for this is the unified CloudWatch agent, not the older, separate CloudWatch Logs agent — that older agent is being phased out, depends on a Python runtime that isn't installed on new instances by default, and AWS's own guidance now points people toward the unified agent instead, especially on instances using IMDSv2. The unified agent is driven by a configuration file that lists which log files to collect: each entry names a file path on disk, the CloudWatch Logs log group to send it to, and the log stream name to use. Add /var/log/cloud-init-output.log as one of those file paths, and from that point forward, every future boot's user data output lands in CloudWatch Logs automatically — searchable from the console, without connecting to the instance at all.
One order-of-operations point worth being honest about: the CloudWatch agent has to already be installed and running before a given boot's user data executes for that boot's output to be captured. It cannot retroactively rescue the log from the failed run you're troubleshooting right now — for that, read the log directly on the instance, or use the console output redirect covered earlier. Where the agent earns its keep is every boot after you set it up: install and configure it once, ideally baked into your AMI or launch template, and every instance launched from that point on ships its own user data output centrally without any further setup.
SSM Run Command and other alternatives to user data
User data is a one-shot bootstrap tool, and it's not always the right one. Three other AWS options solve adjacent problems, and knowing when to reach for each one saves a lot of fighting with a tool that was never meant for the job.
| Tool | Best for | Skip it when |
|---|---|---|
| User data | Light, per-instance customization at first boot: a hostname, a small config file, an environment setting | The setup is heavy, repeated across many instances, or needs to run after launch on instances that already exist |
| Systems Manager Run Command | Running a command on already-running instances on demand, targeted by tag, with per-instance output kept in the console | You need something to happen automatically at first boot with no manual trigger |
| cfn-init (with CloudFormation) | A declarative list of packages, files, and services defined in the same template that manages the rest of the stack, reporting success or failure back to CloudFormation | You're launching a single standalone instance with no CloudFormation stack around it |
| EC2 Image Builder | Baking setup into the AMI itself so instances boot already configured, especially for fleets that autoscale aggressively and can't afford boot-time install time | The configuration changes often, or it's genuinely instance-specific rather than shared across the fleet |
None of these replace user data outright — they solve different points on the same timeline. User data still wins for anything small, per-instance, and needed exactly once at first boot; the other three exist for everything that stops fitting that description.
Checking logs across a whole fleet at once
If a bad launch template rolled out to twenty instances instead of one, connecting to each individually to check cloud-init-output.log is the slow way to find out how far the damage spread. Systems Manager Run Command can run the same read-only command against every instance matched by a tag in a single action, and hand back each instance's output separately in the console.
- Open Systems Manager, choose Run Command, and choose the
AWS-RunShellScriptdocument for Linux instances orAWS-RunPowerShellScriptfor Windows. - Target instances by tag rather than typing individual instance IDs, so any instance launched into the same fleet after the fact is automatically included in future runs too.
- Enter a single read-only command — tailing the last 50 lines of
cloud-init-output.log, for instance — and run it. - Review each instance's output separately in the console. A pattern where every instance fails at the same line points at the script or the launch template itself; a handful of outliers failing differently points at instance-specific problems instead.
This requires the instance to be running the SSM Agent and carrying an instance profile with the necessary Systems Manager permissions — worth checking as part of the same pass if a subset of the fleet doesn't respond to the command at all, since that's often a permissions gap rather than the instances themselves being unreachable. It's the same information a one-by-one SSH check would give you, gathered once instead of N times, and it turns "is this happening everywhere or just this one instance" from a guess into a two-minute check.
Back to Jake's point-of-sale box
Jake's case turned out to be cause 3 — the missing-tags problem. He'd launched the instance through a small internal tool that called the EC2 API directly instead of the console, and that tool passed his script through without wrapping it in <script> tags, because it had been written and tested against Linux instances, where a plain #!/bin/bash at the top is enough. On the Windows instance he'd actually launched, EC2Launch had no idea what to do with a bare batch script with no tags around it, and quietly did nothing.
"So it wasn't broken," Jake said. "It just needed the right wrapper."
"Right," Ethan said. "And that's most of these calls, honestly — it's rarely the instance, rarely AWS, and almost never actually 'broken.' It's one of four or five very specific, very findable things, and the log tells you which one inside the first minute of reading it."
Jake added the tags, re-launched, and the software was installed with twenty minutes to spare before the customer walked in. Before he hung up, he asked Ethan to help him set up the CloudWatch shipping trick for next time — "so I'm not calling you from the shop parking lot again."
Frequently asked questions
Where is the EC2 user data log on Linux?
/var/log/cloud-init-output.log holds your script's actual printed output. /var/log/cloud-init.log holds cloud-init's own internal, module-by-module log, which is what to check if the first file is empty.
Where is the EC2 user data log on Windows?
On AMIs running EC2Launch v2, it's C:\ProgramData\Amazon\EC2Launch\log\agent.log. On older AMIs still running EC2Launch v1, it's C:\ProgramData\Amazon\EC2-Windows\Launch\Log\UserdataExecution.log.
Why does user data only run once?
By design, EC2 treats user data as a first-boot setup mechanism, not a recurring task runner. Running it once by default avoids surprises like software getting reinstalled or accounts getting recreated on every routine restart. You have to explicitly opt in — a MIME multi-part per-boot directive on Linux, or the persist tag on Windows — to make it run every time.
I stopped and started my instance after editing user data. Why didn't the new script run?
Because starting a stopped instance is not the same as launching a new one, and the once-per-instance rule still applies to the same instance ID even after a stop and start. The new user data is saved and will show up if you check it, but it won't automatically execute unless you configure the instance to run user data on every start, or you manually run the updated script yourself.
How do I check what user data an instance actually has?
In the console, select the instance and choose Actions, Instance settings, Edit user data — this shows exactly what's stored, whether or not it's ever run. You can also request it from inside the instance via the instance metadata service, or from outside via aws ec2 describe-instance-attribute --attribute userData.
Can I view or edit user data on a running instance?
You can view it while running. To change it, the instance must first be stopped — EC2 won't let you modify user data on a running instance. Remember that stopping an instance clears any data on instance store volumes, though EBS-backed root volumes are unaffected.
Why does my script fail with "command not found" even though I tested it manually?
User data runs in a minimal, non-interactive shell with a much shorter PATH than an interactive SSH session, and it runs before some setup steps that a logged-in session takes for granted have necessarily finished. A command that works fine when you type it yourself can fail as user data simply because the environment is thinner.
Do I need to add sudo to my user data commands on Linux?
No. User data scripts on Linux already run as root, so prefixing commands with sudo is redundant. It's usually harmless, but it can occasionally interfere with tools that check whether they're already running as root before deciding what to do.
My instance won't boot and I can't SSH in at all. How do I see what happened?
Use Actions, Monitor and troubleshoot, Get system log in the console — it shows the instance's boot console output without needing SSH. For a live, interactive view, use the EC2 Serial Console on supported Nitro-based instance types. For a visual check, Get instance screenshot captures a JPG of the current console.
How much of the console log does AWS actually keep?
Only the most recent 64 KB of posted console output is stored, and it's guaranteed available for at least one hour after the last time anything was posted to it. On a long-running instance, that means "Get system log" may no longer show the original boot output.
How do I get my user data output to show up in the console log?
Add an exec redirect near the top of your Linux script that pipes output to both a log file and /dev/console, using tee and logger. That copies everything your script prints into the same buffer that "Get system log" reads, so you can see it even if you can never SSH into the instance at all.
Why does my Windows user data seem to be completely ignored?
The single most common cause is missing <script> or <powershell> tags around the commands — Windows requires them so EC2Launch knows what kind of interpreter to hand the content to. Plain, untagged commands are silently skipped rather than producing an obvious error.
What's the maximum size of an EC2 user data script?
16 KB, measured on the raw script before base64 encoding. Exceeding it typically blocks the launch outright rather than silently truncating the script. If you're close to the limit, keep the inline script short and have it download a fuller setup script from S3 or a private repository instead.
My script calls the AWS CLI and that part fails. What's usually wrong?
Almost always a permissions gap. The instance needs an instance profile carrying an IAM role with permission to do whatever the script is calling; a script copied from another account or instance can fail purely because the new instance's role is narrower than the one it worked under before.
Does creating an AMI carry my user data forward to new instances?
No. User data is an instance attribute, not part of the disk image, so a new instance launched from your AMI starts with no user data unless you supply it at launch. What does carry forward, if you don't clean it up first, is a leftover copy of the last script cloud-init ran, sitting in the instances directory on disk.
Is it safe to put passwords or secrets directly in user data?
No. User data isn't encrypted, and anyone who can view the instance in your account, or reach its instance metadata service, can read it. Fetch secrets at boot time from Secrets Manager or Parameter Store using the instance's IAM role instead of embedding them directly in the script text.
Revision note. Written September 2026. AWS occasionally renames console menu paths and shifts default agent versions on new AMIs, so if a folder name here doesn't match what you see, check the instance's AMI age first. If you've been staring at a blank terminal wondering what went wrong with a launch that should have "just worked," you're not missing something obvious — this is a genuinely easy thing to get tripped up on, and the log almost always has the answer waiting for you.