A firmware pipeline that has been green all week goes red on a Tuesday. Nothing in the repository changed. The compiler step fails with a licensing error, and it fails on some of the matrix jobs but not all of them.
The cost is immediate. The release branch is blocked. A firmware engineer who was writing driver code spends the morning reading armlm output. The DevOps engineer who owns the runners has no obvious place to start, because the change that caused the failure may be up to seven days old and may have nothing to do with the build.
This is almost never a bug in the pipeline. It is how Arm User-Based Licensing (UBL) works, and it does not work the way FlexNet floating licensing worked. Here is what has to happen for armclang to be licensed on a runner, and what breaks. Seat counts are covered separately in our UBL seat sizing guide, and headless build invocation in the csolution and cbuild guide. Every claim below is sourced to Arm’s published documentation, listed at the end.
The seven-day fuse
Under UBL a licence is not checked out and checked back in. It is a certificate cached on disk with a validity window.
Arm’s User Guide: activating a product creates a licence in the local licence cache for the device, and “This cached license is valid for 7 days.” Each day the development tool runs, it automatically makes one attempt to renew that cache for another 7 days by connecting to the local licence server or the Arm licence server. If it cannot be renewed for 7 days, it becomes invalid.
Then the sentence that explains Tuesday morning:
Unmet network requirements are only reported if the cached license becomes invalid.
An egress rule changed on a Friday afternoon does not fail on the Friday. It fails up to a week later, in a build unrelated to the change, on whichever runner holds the oldest cache. The first red build is not the first broken build, and the window to search is the previous seven days of infrastructure changes, not the last commit.
The cache is a directory: C:\Users\<your username>\.armlm on Windows, /home/<your username>/.armlm on Linux and /Users/<your username>/.armlm on macOS. Nothing else on the runner holds the entitlement.
Three ways to activate, and the one Arm rules out for CI
| Route | What you run or set | Arm’s positioning |
|---|---|---|
| Licence server | armlm activate --server <server_URL> --product <product_code> | For a small number of cloud instances with intranet access |
| Activation code | armlm activate --code <activation_code> | For a small number of cloud instances with internet access |
| Ambient environment variable | ARMLM_ONDEMAND_ACTIVATION=<product_code>@<server_URL> | Carries an explicit caution about CI workflows |
Arm’s caution on that third row:
Using this environment variable for licensing is unsuitable when many devices request license activation in a short period. For example, this could happen in a Continuous Integration (CI) workflow. This type of workflow floods the license server with requests, which can cause the server to reject license requests. For this type of workflow, use Proxy activation to activate the license once and then import the resultant license file on all devices.
The naive CI pattern is exactly that: bake the licence source into the runner image, let every job activate on its own, scale the matrix. Arm is telling you it degrades as you scale it.
The Administration Guide puts a number on it. By default the licence server rate limit is 10 licence requests per second, and Arm’s first remedy for pressure against that limit is a transfer file needing only one licence server request, not a higher limit. That is a Local License Server figure, set by the rate-limit parameter in <installation_directory>/server/local-configuration.yaml. Arm publishes no equivalent figure for the Cloud License Server, so do not plan capacity as though it applied there.
Proxy activation: the pattern Arm points CI at
Proxy activation creates a licence activation file on one device and uses it to provide licences on one or many others, local or in the cloud. Arm names CI in the definition itself, alongside devices that do not meet the network requirements.
Two commands. On a device that meets the network requirements:
armlm activate \
[--server <server_URL> --product <product_code> | --code <activation_code>] \
--as-user <user_name> \
--to-file <transfer_filename> \
[--borrow-period <days>]
Then on each target device:
armlm import --file <transfer_filename> [--enable-renewal]
Four documented behaviours make this the right shape for ephemeral runners.
One licence server request, not one per runner. Exactly the remedy Arm recommends against the rate limit.
The validity window is adjustable. --borrow-period <days> extends the time a licence cache is valid from the default of 7 days, and <days> can be any integer from 7 to 365. It requires the transfer file to be created with Arm licensing utilities version 1.3.2 or later.
No network calls by default. Arm: “By default, licenses created by proxy activation do not attempt to renew the license, stopping all attempts to access a network.” --enable-renewal on armlm import allows renewal attempts every 24 hours, and on renewal the cache validity is set to 7 days, overriding --borrow-period. A licence you deliberately borrowed for 90 days silently collapses to 7.
It is not fire and forget. If a target device needs the licence for longer than the renewal period, the proxy activation process must be repeated before that period expires. A long borrow also locks the seat: activating a product makes the licence unavailable to other users, and extending validity extends that lock.
A CI cache artifact is not one of Arm’s four documented ways to share one user’s licence across devices. Treat the transfer file as licence material, not configuration.
Identity: whoami decides whether the build is licensed
UBL binds the entitlement to a user, with no limits on concurrent usage, including the same product on multiple devices. Arm’s own example is a service account building and testing on any number of devices, and a seat is exactly one per user regardless of device count. Read that as a licensing permission, not a benchmark: Arm publishes no supported concurrency ceiling.
What breaks first is not the count. It is the name.
Arm defines error 400 - Unauthorized user as the user identifier associated with the runtime process of the tool not matching the information in the licence. The listed causes: a username that differs across devices, a licence cache copied from another user, and a relocated cache directory populated with another user’s licence information. Arm’s first diagnostic step is to check the username by running whoami in a console window, and Arm states the username activating the product must be identical on all devices. On licence server version 1.2024010.0 or earlier it is also case-sensitive.
That is what --as-user has to match: the operating system username of the process that executes armclang. Not the GitHub account, not the service principal in your identity provider. Container base images, USER directives, sudo steps and job-level container: blocks all move that value without announcing it.
whoami && armlm inspect
armlm inspect lists active and deactivated licences with their product or activation codes. Two lines at the top of a job, before anything expensive, turn a mid-build failure into an immediate and readable one.
One correction most teams need: deactivation does not free a seat. Arm states it is an activity local to a single device and does not release the licence for another user. For a licence server, a licence is automatically released for another user only when it has not been renewed for a minimum of 7 days. Arm’s own ARM-software/cmsis-actions/armlm Action has inputs code, server and product, no deactivate input and no post-job step, and Arm’s CMSIS_6 CoreValidation workflow activates and never deactivates.
Containers and credentials
Arm’s Docker learning path ships a Dockerfile with the licensing block commented out and three options offered:
# License configuration
# Uncomment and modify below as appropriate
#
# ENV ARMLM_ONDEMAND_ACTIVATION=product_code@https://internal.ubl.server
# or
# RUN armlm activate --code <activation-code>
# or
# ENV ARMLMD_LICENSE_FILE=port@server
The first is the variable Arm flags as unsuitable for CI. The second writes an activation code into an image layer, where it survives every docker history and registry copy. The third is the legacy FlexNet variable, not UBL at all.
The rule for your container standards: licence material is runtime state, not image content. An image should contain the tools and armlm, never an activation code, a licence server credential or a populated .armlm.
Two shapes satisfy that using only documented mechanisms:
- Mount the cache. Set
ARMLM_CACHED_LICENSES_LOCATIONto a path mounted at run time, holding a cache activated for the identity the container runs as. Arm’s caveat applies: activation and renewal are slower on a network location. - Import at start. Ship no licence in the image. At container start, fetch the transfer file from your secret store and run
armlm import --file <transfer_filename>. Regenerate on a schedule shorter than the borrow period.
For credentials, Arm’s own workflow is the reference implementation: CMSIS_6 CoreValidation passes ${{ secrets.ARM_UBL_ACTIVATION_CODE }} to the ARM-software/cmsis-actions/armlm Action. Two things to get right around it. The Action’s product: input defaults to KEMDK-COM0, a non-commercial edition, so a commercial pipeline must supply its own activation code, or its own server URL plus the product code for its entitlement. And an activation code is not associated with any user or service account until its first use, so the first job that runs with it decides who owns it. Fixing that is administrative: the administrator revokes the code and issues a new one, after which cached licences usually take up to 7 days to expire, longer for proxy-activated ones.
The firewall ask, in a form a network team can action
“Allow arm.com” is not actionable. This is.
For activation-code licensing, a device requires internet access to https://api.arm.com/p-software-licensing and https://arm.compliance.flexnetoperations.eu/instances. Arm’s firewall note: protocol HTTP with SSL, TCP port 443, hostnames api.arm.com and arm.compliance.flexnetoperations.eu, and the use of IP addresses is not recommended because the IP addresses of the URLs are not static. Hand that last clause to anyone asking for a CIDR range.
curl https://api.arm.com/p-software-licensing/v1/health
# expect: { "health": "up" }
curl https://arm.compliance.flexnetoperations.eu/api/1.0/instances/ping
# expect output including: "connectionCheck" : "success"
Run those from the runner, not from a laptop on a different network.
For a Local License Server the ask is smaller: users require access to the IP address and port of the licence server for activation and renewal, and Arm notes this is the only licence server port users require. The default is TCP 7070, settable at install. One line usually closes the security review: during activation or renewal, the only personal data sent to the local licence server or the Arm licence server is the username.
Build networks with no internet access have a documented path rather than a workaround. Proxy activation needs one device with licence server or activation code network access to create the file, plus a way of copying it to the devices that need tool access. With a borrow period of up to 365 days and no renewal attempts by default, an isolated build farm can run on it.
Triage: symptom to cause
| Code | Symptom | What Arm says to do |
|---|---|---|
100 | No licence found in the local cache | Activate with the information from your licence administrator. In CI, the cache usually did not reach the runner, or the process runs as a different user than you think |
200 | Expired cached licence | Three causes: no network connection to the licence server, a cache that cannot be modified (disk space or permissions), or a changed licence server URL. Where network requirements cannot be met, the fallback is proxy activation |
400 | Unauthorized user | Check whoami. The username must be identical on all devices, and is case-sensitive on licence server version 1.2024010.0 or earlier |
501 | Library communication error | Check disk space and that a home directory exists, reinstall or update the tool, delete the cache directory and activate again |
| Product unsupported | e.g. armclang: error: FuSa versions of the Product is not supported with this license | Occurs with a licence server, or after upgrading to a tool version the cached licence does not support. If the licence was proxy-activated, proxy-activate again; otherwise the cache picks the change up on renewal |
That last row is the one a pinned-toolchain pipeline hits: on a proxy-activated estate, tool upgrades and licence refreshes are the same change window.
The environment-variable surface
Arm publishes exactly five variables. Knowing the list is complete is as useful as knowing what is on it.
| Variable | Purpose |
|---|---|
ARMLM_CACHED_LICENSES_LOCATION | Full path to the directory containing the licence files. Default: .armlm in the user home directory |
ARMLM_ECHO_LOG_CONTENT | Utilities 1.3.2 or later. true echoes log errors to stdout and stderr, which is what makes a licensing failure visible in a CI log |
ARMLM_LOG_ENABLED | true logs all licensing operations to <license_directory>/logs. Default: errors only |
ARMLM_MAX_LOG_FILES | Number of log files kept. Default 10 |
ARMLM_ONDEMAND_ACTIVATION | <product_code>@<server_URL>. Carries Arm’s CI caution |
There is no documented proxy variable. If your runners cannot reach api.arm.com because a corporate proxy sits in the way, the sourced answers are proxy activation with a transfer file, or a Local License Server on the reachable side.
UBL does not apply to Arm development tools released before 2022. That migration is its own decision, covered in our FlexNet node-locked migration guide.
The checklist
- Run
whoamiandarmlm inspectas the first step of a job. Confirm the identity the licence is bound to is the identity the compiler runs as. - Decide whether your matrix is small enough for per-job activation or large enough for proxy activation.
- If you go proxy, choose
--borrow-perioddeliberately, remember--enable-renewalresets the window to 7 days, and schedule regeneration of the transfer file. - Move the activation code and transfer file out of images and into your secret store.
- Send the network team hostnames, protocol and port with the two
curlchecks, not an IP range. - Widen the search window to seven days when triaging a licensing failure.
How GSAS helps
GSAS Micro Systems is the authorized Arm development tools engineering partner for India, the UAE and Sri Lanka, and has worked with Arm since 2018 across Keil MDK, Arm Compiler for Embedded, Arm Compiler for Embedded FuSa and Arm Development Studio.
The value is engineering, not paperwork. Our field application engineers work in your time zone, on your build farm, starting from the shape of the log.
| What you are facing | What GSAS does |
|---|---|
| Pipeline failing on licensing, not reproducible locally | Pipeline review: we read the job, the identity and the cache, and find where the entitlement stops reaching the compiler |
| Matrix outgrowing per-job activation | Migration to proxy activation, including borrow period, regeneration schedule and ownership |
| Egress policy blocking activation | Proxy and firewall configuration with your network team, using the documented hostnames, ports and reachability checks |
| Air-gapped defence, aerospace and secure-medical builds | Local License Server deployment alongside your IT security team |
| Uncertain seat counts | Seat sizing against your real job and identity patterns rather than guesswork |
| Still on FlexNet | Migration planning from FlexNet licensing to UBL, sequenced with your toolchain upgrades |
| Licence material in image layers | Container and runner standards so it stops happening |
Our engineers work with teams across Bengaluru, Hyderabad, Chennai, Pune, Mumbai, Delhi NCR and Visakhapatnam, and across the UAE and Sri Lanka.
Talk to GSAS about your Arm UBL CI setup
For seat counts rather than mechanics, start with the UBL seat sizing guide. For the build steps themselves, see the csolution and cbuild pipeline guide.
Sources
Every claim in this article traces to one of the following Arm publications, read in the versions noted.
- Arm, User-based Licensing User Guide, document 102516. Read as PDF
102516_1.4_01_en, issued 2025-10-31. Current version: https://developer.arm.com/documentation/102516/latest/ - Arm, User-based Licensing Administration Guide, document 107573. Read as PDF
107573_1.2024500_04_en, issued 2025-04-30. Current version: https://developer.arm.com/documentation/107573/latest/ - Arm, Arm Software Licensing install guide, which restates the cache window in plainer language (“A user-based license is cached locally for up to 7 days”): https://learn.arm.com/install-guides/license/
- Arm, Prepare Docker image for Arm embedded development, Arm Learning Paths: https://learn.arm.com/learning-paths/embedded-and-microcontrollers/docker/dockerfile/
- Arm,
ARM-software/cmsis-actions(README andarmlm/action.yml): https://github.com/ARM-software/cmsis-actions - Arm,
ARM-software/CMSIS_6CoreValidation workflow: https://github.com/ARM-software/CMSIS_6/blob/main/.github/workflows/corevalidation.yml
Arm revises this documentation regularly. Where a figure matters to a decision you are making, check it against the current version at the two latest links above before you commit to it.
Also appears in:
Interested in Arm tools?
Talk to our application engineers for personalized tool recommendations.
More from Arm
View all →