Troubleshooting#
This page collects the questions that come up most often when computations do not behave as expected, and the client methods that answer them.
Most problems fall into one of three categories: the computation is not being picked up, the computation ran and failed, or the client cannot talk to the server.
A record is stuck in “waiting”#
A record with a status of waiting has been created but it is not being claimed. See
Record Statuses. This is normal for a short while, but a record that stays waiting
indefinitely means no compute manager is able to take its
task.
The server can answer this directly. get_waiting_reason()
takes a record ID and reports why the record has not been claimed, checking each active manager in
turn. get_waiting_reason() does the same from a record
object.
>>> client.get_waiting_reason(118326390)
{'reason': 'No manager matches programs & tags',
'details': {'test_cluster-a_host1-1234-5678': "Manager missing programs: {'psi4'}",
'test_cluster-a_host2-8765-4321': 'Manager does not handle tag "big_mem"'}}
>>> # Or from the record
>>> r = client.get_records(118326390)
>>> r.get_waiting_reason()
{'reason': 'Waiting for a free manager',
'details': {'test_cluster-a_host1-1234-5678': 'Manager is busy'}}
The reason key is always present and holds the overall answer. When the record really is
waiting and there is at least one active manager, a details key is also present, mapping each
active manager’s name to why that particular manager cannot take the task.
The possible values of reason are:
Reason |
What it means |
|---|---|
|
No record with that ID on the server |
|
The record’s status is something other than |
|
Services are iterated by the server, not claimed by a manager. See A service is not progressing |
|
No manager is currently connected to the server |
|
Managers are active, but none has both the required programs
and a matching compute tag. Check |
|
A manager could take this task but is at capacity. This is normal - the task should run once the manager has room |
The two entries to act on are the details messages:
Manager missing programs - the task requires a program the manager does not have installed.
The required program comes from the record’s specification, so a singlepoint with
program="psi4" needs a manager with psi4 available. Managers report what they have, which you
can check with query_managers().
>>> for m in client.query_managers(status='active'):
... print(m.name, m.compute_tags, sorted(m.programs.keys()))
test_cluster-a_host1-1234-5678 ['big_mem'] ['geometric', 'qcengine']
test_cluster-a_host2-8765-4321 ['*'] ['psi4', 'qcengine', 'rdkit']
Manager does not handle tag - the record’s compute tag is not one the
manager is configured to claim. Note that a task tagged * is claimed only by a manager that
also has *; the wildcard is not a “match anything” on the task side. See Compute tags.
A record’s tag can be changed after the fact with
modify_records().
>>> r = client.get_records(118326390)
>>> print(r.task.compute_tag, r.task.required_programs)
big_mem ['psi4', 'qcengine']
>>> client.modify_records(118326390, new_compute_tag='*')
query_active_managers() answers the same question ahead of
time - given a tag and a set of programs, which managers could take such a task.
>>> client.query_active_managers(compute_tag=['big_mem'], programs={'psi4': []})
[]
>>> client.query_active_managers(compute_tag=['*'], programs={'psi4': []})
['test_cluster-a_host2-8765-4321']
Note
get_waiting_reason only considers managers the server currently regards as active. A
manager that has stopped reporting in is eventually marked inactive, and any tasks it had
claimed are returned to waiting to be claimed by someone else.
A record has an error#
A record with a status of error ran and failed. The details are on the record itself.
error- a dictionary witherror_typeanderror_message. Usually the most useful thing to readstdout- the program’s output, if it was storedstderr- anything written to stderr
>>> r = client.get_records(118326390)
>>> print(r.status)
RecordStatusEnum.error
>>> print(r.error["error_type"])
unknown_error
>>> print(r.error["error_message"])
QCEngine Unknown Error:
...
psi4.driver.p4util.exceptions.SCFConvergenceError: Could not converge SCF iterations in 200 iterations.
A record may have been attempted more than once. Each attempt is an entry in
compute_history, and the top-level error,
stdout, and stderr reflect the most recent one. Earlier entries are useful when a record has
been reset and failed differently each time, and the manager_name on each entry tells you
where it failed - a program failing on one cluster but not another is a strong signal.
See Base Record.
>>> for h in r.compute_history:
... print(h.modified_on, h.status, h.manager_name)
2026-04-19 15:14:31.128937+00:00 RecordStatusEnum.error test_cluster-a_host1-1234-5678
2026-04-21 09:02:11.551204+00:00 RecordStatusEnum.error test_cluster-a_host2-8765-4321
To find the errored records among many, query by status, or use a dataset’s status methods.
>>> for r in client.query_records(status='error', limit=10):
... print(r.id, r.record_type, r.error["error_type"])
118326390 singlepoint unknown_error
118326404 optimization compute_error
>>> # Within a dataset
>>> ds = client.get_dataset_by_id(377)
>>> ds.print_status()
specification complete error
--------------- ---------- -------
b3lyp/def2-svp 18 2
If an error looks transient - a node failure, a network problem, a queue timeout - reset the record
to run it again with reset_records(). A server may also be
configured to reset errored records automatically; see
Automatic resets (auto_reset).
>>> client.reset_records(118326390)
UpdateMetadata(error_description=None, errors=[], updated_idx=[0], n_children_updated=0)
Hint
If an error is not transient - an unknown method, a basis set the program does not have, a molecule that will never converge - resetting will just fail again. Fix the specification and submit a new record instead.
Note
A compute manager disappearing does not normally produce an error. Those records go back to
waiting instead, and there is nothing to reset.
A service is not progressing#
A service - a torsiondrive, gridoptimization, NEB, manybody, or reaction
record - is iterated by the server rather than claimed by a manager, so
get_waiting_reason() will only tell you
Record is a service. See Tasks and Services.
A service alternates between waiting on the records it created and iterating. The usual reason it appears stalled is that one of those dependencies is itself stuck or errored.
>>> r = client.get_records(118326390)
>>> print(r.is_service)
True
>>> # What is it waiting on?
>>> dep_ids = [d.record_id for d in r.service.dependencies]
>>> for dep in client.get_records(dep_ids):
... print(dep.id, dep.status)
118326391 RecordStatusEnum.complete
118326392 RecordStatusEnum.error
Once a stuck dependency is dealt with, the service will pick up again on its next iteration.
Services also write progress to stdout as they run,
which is worth reading - unlike task-based records, this is updated while the record is still
running.
Two server settings limit how quickly services iterate: max_active_services caps how many run
at once, and service_frequency sets how often the server looks for services to iterate. On a
busy server a service may simply be queued behind others. See Server Configuration.
The client cannot reach the server#
Failed requests raise PortalRequestError, which carries the HTTP
status code and the server’s message.
>>> from qcportal import PortalRequestError
>>> try:
... r = client.get_records(1)
... except PortalRequestError as e:
... print(e.status_code)
... print(e.msg)
404
Request failed: Could not find record with id 1
Common cases:
401 / 403 - not logged in, or the role does not permit the operation. Check the username and password in use, and see Roles for what each role allows
404 - the record, dataset, or project does not exist. Many client methods accept
missing_ok=Trueto returnNoneinstead of raising400 - the server rejected the request. The message is usually specific
500 - an internal server error. See Internal server errors
If the connection fails outright rather than returning a status code, the address or the
credentials are the place to start. get_server_information()
is a cheap way to confirm you are talking to the server you think you are.
>>> print(client.address)
https://ml.qcarchive.molssi.org/
>>> info = client.get_server_information()
>>> print(info["version"])
0.65
See QCPortal Installation & Setup and Connecting to a server for how the address and credentials are resolved, including from configuration files and environment variables.
Retrieval is slow#
Repeatedly fetching the same records is the most common cause of a slow script.
Enable an on-disk cache by passing
cache_dirto thePortalClientconstructor. Records and datasets are then reused across runs instead of being downloaded again. See Caching & ViewsFetch in bulk.
get_records()takes a list of IDs and retrieves them in one request; a loop of single-ID calls does one request eachUse
include=['**']when you know you will need all of a record’s data, so it arrives in the initial fetch rather than as a separate request per field laterFor a finished dataset, a view is a single local file with no server round-trips at all
Hint
Queries return an iterator, not a list, and fetch in batches as you
iterate. Calling list() on a query with no limit will try to pull everything.
Internal server errors#
Errors inside the server itself are recorded in a separate log from record errors. By default the
server does not show the details to users - the hide_internal_errors setting - and instead
returns an error ID to quote to an administrator. See Server Configuration.
Users with sufficient permissions can read that log with
query_error_log().
>>> for e in client.query_error_log(limit=5):
... print(e.id, e.error_date, e.user, e.request_path)
12 2026-08-01 14:22:03.118293+00:00 ben /api/v1/records/bulkGet?
>>> # Or look up the ID a user was given
>>> entry = next(iter(client.query_error_log(error_id=12)))
>>> print(entry.error_text)
Traceback (most recent call last):
...
Entries are ErrorLogEntry objects, and the query can be
filtered by error_id, user, before, and after. Old entries are removed with
delete_error_log(), which takes a cutoff date and returns the
number deleted.
>>> from datetime import datetime
>>> client.delete_error_log(datetime(2026, 1, 1))
41