Troubleshooting
Every configuration snippet below uses options that actually exist. Since #105 the provider rejects unknown keyword arguments:
ProviderConfigurationError: Unknown configuration option(s): use_ssh_tunnel.
Check the spelling against EphemeralProvider.__init__; an option accepted
here but never read would be silently ignored.
If you see that, the option is not real — check
api_reference.rst. Older versions of this document
recommended use_ssh_tunnel, security_group_ingress, verify_ssl,
fallback_to_on_demand, max_cost_per_hour, and a dozen others that were never
implemented; they now fail loudly instead of being ignored.
Start here
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("parsl_ephemeral_provider").setLevel(logging.DEBUG)
Or pass debug=True to the provider. Add logging.getLogger("botocore").setLevel( logging.DEBUG) when you need to see the raw AWS calls — it is extremely verbose.
Then, in order:
Confirm credentials resolve.
aws sts get-caller-identity --profile <p>.Confirm the network IDs exist in the right region. Construction validates them, so a
ResourceNotFoundErrorhere is the answer, not a symptom.Confirm what the provider thinks it owns —
provider.list_resources().Get onto the instance.
aws ssm start-session --target i-...needs no key pair and no inbound rule;/var/log/cloud-init-output.logholds theworker_initoutput.
Configuration errors at construction
Unknown configuration option(s): ...
The option does not exist. Common renames:
Old / imagined |
Real |
|---|---|
|
|
|
|
|
|
|
|
|
not accepted — use the environment or a profile |
|
|
|
|
|
|
|
|
|
|
|
removed — the network is always yours (#69) |
Eight options that were unreachable before v0.8.0 are now accepted:
idle_timeout, preserve_bastion, bastion_host_type, and workflow_id on
mode="detached"; lambda_runtime, ecs_task_cpu, ecs_task_memory, and
ecs_container_image on mode="serverless" (#136). Each is accepted only on the
mode that implements it — setting one elsewhere raises
ProviderConfigurationError rather than silently keeping the default.
lambda_memory and lambda_timeout remain spelled memory_size and timeout
at the provider.
Since v0.10.0 that guard also covers the four options #136 left alone (#155):
bastion_instance_type on detached mode, memory_size and timeout on
serverless. They had been accepted on every mode since before the guard existed,
so this is a compatibility break — code that passed one harmlessly now raises,
which is the point: there is no longer a second class of option that looks
configured and is not. compute_type is handled differently; see below.
vpc_id, subnet_id, security_group_id are required
As of v0.7.0 the provider never creates network resources. Supply your own; see network-prerequisites.md. The serverless-plus-Lambda combination is the sole exception — Lambda functions run in the Lambda-managed VPC, so none of the three is needed.
ResourceNotFoundError: subnet_id subnet-... is not usable
The ID is malformed, deleted, or in a different region or account. Construction
verifies each ID with describe_*. Before v0.7.0 the provider silently set the
attribute to None and failed much later inside RunInstances with an opaque
InvalidParameterValue.
Invalid compute type: auto
compute_type accepts ec2, lambda, or ecs. There is no auto at the
provider level; leaving it at the ec2 default in serverless mode leaves
ServerlessMode on its internal auto heuristic. Set lambda or ecs
explicitly.
compute_type='lambda' is supported only by mode='serverless'
Standard and detached mode launch EC2 instances, so ec2 is the only value they
can honour (#155). The mirror case cannot be an error, because it is the default:
compute_type="ec2" on mode="serverless" warns instead, since
ServerlessMode ignores the value and falls back to its own auto. If you see
that warning and did not intend auto, pass lambda or ecs.
TypeCheckError: argument "mode" ... is not an instance of str
mode is a string — "standard", "detached", "serverless". Passing
StandardMode(...) does not work; the provider constructs the mode itself so it
can inject the session, state store, and resolved AMI.
warm_pool_size ... is supported only by mode='standard'
warm_pool_size, warm_pool_ttl, bake_ami, baked_ami_id, and one_shot are
implemented by StandardMode alone. Setting them on another mode used to leak
instances no mode would reclaim (#80), so it is now rejected.
warm_pool_size > 0 requires ...
Warm-pool and one-shot dispatch both go over SSM SendCommand, so the instance
needs AmazonSSMManagedInstanceCore. Pass either
auto_create_instance_profile=True or an explicit iam_instance_profile_arn.
Workers launch but never register
By far the most common failure, and it is almost never on the AWS side.
Cause. HTEX workers connect outbound to the interchange running on your client. The client must therefore accept inbound TCP on ports 54000–55000. A laptop behind home or office NAT cannot, no matter how the worker security group is configured.
Diagnosis. The instances reach running, worker_init completes cleanly in
/var/log/cloud-init-output.log, and runinfo/*/ on the client shows no worker
ever connecting.
Fixes, in order of preference:
Run the client on an EC2 instance in the same VPC, with a security group that allows inbound 54000–55000 from the worker security group. Note the default VPC security group allows inbound only from itself, so if workers use a different group you need an explicit rule.
Use detached mode — a bastion owns the worker lifecycle, and the client need not be reachable at all.
Use one-shot mode for independent commands; it bypasses HTEX entirely and works from anywhere.
Also check encrypted=False:
HighThroughputExecutor(label="aws", provider=provider, encrypted=False)
With encryption on, Parsl generates CurveZMQ certificates in the client’s
run_dir, which workers cannot read — so they fail to register with no obvious
error. Certificate distribution is
#62.
Interchange address. On EC2, an elastic or public IP is not bound to the
interface; only the private IP is. Use address_by_route() for a same-VPC
deployment. address_by_query() returns the NAT/router WAN address, which nothing
in the VPC can connect back to.
Instance launch failures
InsufficientInstanceCapacity
AWS has no capacity for that type in that Availability Zone right now.
provider = EphemeralProvider(
# ... network options ...
use_spot=True,
use_spot_fleet=True,
instance_types=["m5.large", "m5a.large", "m6i.large", "c5.large"],
spot_allocation_strategy="price-capacity-optimized",
)
instance_types is a list of type names — not {"type": ..., "weight": ...}
dicts, and the provider does not synthesize alternatives from instance_type.
Note the fleet is confined to the single subnet you pass, so it stays in one AZ;
multi-AZ diversification is not exposed. To move AZ, pass a subnet in a different
one.
Unsupported: The requested configuration is currently not supported
Usually an instance type that does not exist in the AZ your subnet is in. t3.micro
is unavailable in us-east-1e, for example. Check with:
aws ec2 describe-instance-type-offerings --location-type availability-zone \
--filters Name=instance-type,Values=t3.micro --region us-east-1
Instances launch and terminate immediately
Read /var/log/cloud-init-output.log over SSM. A worker_init failure — a
missing package, a wrong Python version — is the usual cause. Note the instances
run with InstanceInitiatedShutdownBehavior="terminate", so a shutdown inside
worker_init destroys the instance rather than stopping it.
AMINotFoundError
The AMI is resolved from AWS’s public SSM parameters, so this normally means the
ssm:GetParameter call failed — check that your credentials allow
ssm:GetParameters on /aws/service/ami-amazon-linux-latest/*, or pass
image_id explicitly. An arm64 AMI is selected automatically for Graviton
instance types.
Cannot submit job, already at max_blocks = N
max_blocks caps concurrent submissions, and the provider raises rather than
queueing. Raise max_blocks, or let jobs finish first.
Spot instances
Frequent interruptions
Diversify across families and generations rather than sizes:
provider = EphemeralProvider(
# ... network options ...
use_spot=True,
use_spot_fleet=True,
instance_types=["m5.xlarge", "m5a.xlarge", "m5n.xlarge", "m6i.xlarge"],
spot_allocation_strategy="price-capacity-optimized", # the default
spot_interruption_handling=True,
)
Keep price-capacity-optimized. lowest-price trades interruption rate for cost
and is usually the wrong choice for a workflow. Set retries on the Parsl
Config so reclaimed tasks are re-run.
There is no hibernate-on-interruption support and no on-demand/spot mix —
spot_interruption_behavior and on_demand_percentage do not exist.
Interruptions are only noticed after the fact
Set spot_interruption_handling=True. The provider then creates an EventBridge
rule matching EC2 Spot Instance Interruption Warning with an SQS target and
polls it, giving the full two minutes of notice. Without it, the first sign is the
instance already in shutting-down.
An instant EC2 Fleet cannot use Capacity Rebalance — CreateFleet rejects
SpotOptions.MaintenanceStrategies for that type — which is why the warning comes
via EventBridge rather than from the fleet itself.
The fleet is created but launches nothing
Capacity was unavailable at your price. Check describe_fleets for the errors,
add instance types, and raise or unset spot_max_price_percentage.
InvalidParameterValue on the allocation strategy
CreateFleet wants kebab-case (price-capacity-optimized). The camelCase
spelling (priceCapacityOptimized) belongs to the legacy Spot Fleet API and the
CloudFormation templates. Neither DryRun nor a zero-capacity request validates
the enum, so this only surfaces on a real launch.
Permissions
Credentials not found
The provider resolves credentials through botocore; it accepts no key arguments.
Use AWS_PROFILE, ~/.aws/credentials, an instance profile, or
profile_name="myprofile". CredentialResolutionError carries the reason —
before v0.7.0 that message was swallowed by a TypeError.
iam:PassRole denied
auto_create_instance_profile=True creates a role and passes it to EC2. Grant
iam:PassRole scoped with iam:PassedToService: ec2.amazonaws.com, or supply
iam_instance_profile_arn for a role you manage.
State persistence
s3_bucket is required when using 's3' state store
Pass s3_bucket. By default the provider does not create the bucket — create it
yourself and set default encryption on it, or pass s3_create_bucket=True to have
the provider create it when absent (it applies a public-access block and tags, but
not default encryption).
State is lost or two runs fight over resources
Use one state location per workflow. Two providers pointed at the same
state_file_path, s3_key, or parameter_store_path adopt each other’s
provider_id and then compete over the same instances. Conversely, that adoption
is exactly how deliberate reconnection works.
Restart re-bakes the AMI, or loses the job map
Fixed in v0.7.0 (#78). The provider and the mode used to overwrite each other’s
state document — the provider wrote provider_id/job_map, the mode wrote the
baked-AMI and warm-pool fields, and each full-overwrite destroyed the other’s
keys. State is now namespaced by key; flat v0.6.0 documents are still read.
If you have a stale ephemeral_aws_state.json from an older version, deleting it
is safe once you have confirmed the resources it names are gone:
parsl-ephemeral-cleanup --dry-run --region us-east-1
StateStoreError on the file backend
Check directory permissions and that the path is not on a filesystem without
flock support — the file store serializes with fcntl.flock.
Serverless mode
Lambda tasks fail with an import error
worker_init has no effect on Lambda; there is no instance to run it on.
Dependencies must be in the deployment package or a layer.
Lambda tasks time out
Raise timeout (seconds). Lambda’s own ceiling is 900 s. memory_size also
governs CPU — a task that is slow rather than blocked often just needs more
memory.
Fargate tasks exit immediately
Read the task’s stoppedReason and its CloudWatch Logs.
If the command fails on a missing import, the default image (python:3.12-slim)
carries the standard library only — set ecs_container_image to one with your
dependencies. Before v0.8.0 the default was public.ecr.aws/lambda/python:3.9,
a Lambda base image whose entrypoint is the runtime interface emulator, so it
expected an invocation event rather than the task’s command; if you pinned that
image deliberately, that is why tasks exit at once (#136).
Also check that ecs_task_cpu and ecs_task_memory are a combination Fargate
accepts — an invalid pair fails the task definition, not the task.
Spot in serverless mode
Supported, in two different forms, and compute_type decides which:
use_spot=Truewithcompute_type="ecs"sets the cluster’s capacity provider toFARGATE_SPOT. Still serverless — no instances of yours.use_spot_fleet=Truewithcompute_type="ecs"is something else entirely: it bypasses ECS and launches aninstantEC2 Fleet per job, so you are back to managing instances.instance_typesandspot_max_price_percentageapply.compute_type="lambda"ignores both. Lambda has no spot pricing.
Detached mode
The bastion never comes up
Read the CloudFormation stack events:
aws cloudformation describe-stack-events --stack-name parsl-bastion-<id> --region us-east-1
Then SSM to the bastion and read its journal. The bastion needs outbound access
for the SSM agent to register — a private subnet requires a NAT gateway or the
ssm/ssmmessages/ec2messages VPC endpoints.
The bastion is up and healthy, but no worker ever launches
Check the manager service’s uptime, not whether it is active:
aws ssm start-session --target i-<bastion> --region us-east-1
sudo systemctl show parsl-bastion-manager -p ActiveEnterTimestamp -p NRestarts
sudo journalctl -u parsl-bastion-manager -n 50
systemctl is-active reports active for a service that is crash-looping under
Restart=always, so it cannot tell a working manager from one that dies at every
start. A high NRestarts or an ActiveEnterTimestamp seconds old is the tell. Two
other signals mislead here as well: the UserData sentinel file is touched before the
manager starts, and SSM PingStatus: Online says nothing about the instance profile
when Default Host Management Configuration is enabled, since DHMC registers
instances that have no profile at all.
Before v0.10.0 the direct (bastion_host_type="direct") path attached no instance
profile, so the manager died on NoCredentialsError every ten seconds forever
(#229). If the
journal shows NoCredentialsError, or AccessDenied naming an EC2, fleet,
launch-template, or ssm:GetParametersByPath call, upgrade — or supply a profile of
your own via bastion_instance_profile_arn.
The bastion is still running after the workflow finished
That is deliberate: the bastion is preserved so you can reconnect. Call
provider.shutdown() when the workflow is genuinely over, or pass
preserve_bastion=False so shutdown terminates it. Its own idle-shutdown timer
is idle_timeout (minutes, default 30). The provider’s max_idle_time is
unrelated, and is deprecated and ignored
(#194).
Cost
Resources are still running after the process exited
There is no atexit hook. Nothing cleans up when the interpreter exits, and
parsl.clear() releases Parsl’s resources, not AWS ones. Call
provider.shutdown(), and prefer a try/finally.
To find orphans from a crash:
parsl-ephemeral-cleanup --dry-run --region us-east-1 # then without --dry-run
It sweeps by tag — both CreatedBy=ParslEphemeralProvider (standard mode) and
ParslResource=true (detached and serverless) — so it finds resources the state
file no longer names. It ships with the package, so it is on PATH after any
install, not only in a git clone.
The bill is higher than expected
The provider has no cost monitoring — max_cost_per_hour and
enable_cost_monitoring do not exist. What it does have:
min_blocks=0so nothing runs when nothing is queuedmax_blocksas a hard ceilingauto_shutdown=Trueso a worker terminates itself once its command finishesmax_idletimeon your ParslConfigto reclaim workers that are idle but upuse_spot=True, optionally withspot_max_price_percentage
Set AWS Budgets and a CloudWatch billing alarm; tag with additional_tags for
cost allocation.
Two things to know specifically:
Warm-pool instances are held
Runningand bill at the full rate for up towarm_pool_ttlseconds per idle period. Native ASG warm pools, which hold instancesStopped, are #130.A role created by
auto_create_instance_profile=Trueis deleted on shutdown since v0.8.0 (#132). If you still see them accumulating, your IAM policy is missing the teardown actions: cleanup logs rather than raises, soAccessDeniedthere is silent (#195). Generate a current policy withEphemeralComputeProvider.minimum_iam_policy(), and reap existing orphans withparsl-ephemeral-cleanup.Bastion roles were a second, quieter instance of the same leak until v0.10.0 (#229). The teardown removed managed policies only, and
delete_rolerefuses while an inline policy remains — which is what the bastion role carries — so every bastion pair survived while the log said nothing.parsl-ephemeral-cleanupnow sweepsparsl-bastion-{role,profile}-*alongside the worker pairs.
Stopped instances with billed EBS volumes
Fixed in v0.7.0. Standard mode never set
InstanceInitiatedShutdownBehavior, so EC2’s stop default applied to the
shutdown -h now that one-shot mode appends — leaving a stopped instance and a
billed volume that the provider had already dropped from tracking, because
stopped maps to COMPLETED. All launch paths now set terminate. Sweep for
pre-v0.7.0 leftovers with the cleanup tool.
Slow startup
The default worker_init installs Python 3.11 and Parsl on every launch, which
dominates a short workflow’s wall clock. Options, in increasing effectiveness:
Bake an AMI — standard mode,
bake_ami=True. Runsworker_initonce into a custom image at construction; every later launch skips it. Passbaked_ami_id="ami-..."to reuse the image across runs.Use the warm pool —
warm_pool_size=N, standard mode. Finished instances are reused instead of relaunched. Read the cost note above.Bring your own AMI —
image_id="ami-..."with everything pre-installed.Keep
worker_initminimal. Install only what the tasks import.
AWS API throttling
RequestLimitExceeded, or botocore logging “max retries” — the provider wraps AWS
calls in exponential backoff with jitter, so occasional throttling is absorbed.
If it is persistent, raise status_polling_interval (default 60 s) and
waiter_delay (default 5 s), and lower max_blocks.
Getting help
Open an issue at github.com/scttfrdmn/parsl-ephemeral-provider/issues with:
What you ran, including the full provider configuration with IDs redacted
The full traceback
The provider version (
python -c "import parsl_ephemeral_provider; print(parsl_ephemeral_provider.__version__)") and the Parsl version/var/log/cloud-init-output.logfrom a worker, if the workers launched
For a suspected vulnerability, open a security advisory instead.
SPDX-License-Identifier: Apache-2.0 SPDX-FileCopyrightText: 2025-2026 Scott Friedman and Project Contributors