diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py
index da11b7f..762658a 100644
--- a/dwave/cloud/solver.py
+++ b/dwave/cloud/solver.py
@@ -204,17 +204,35 @@ class BaseSolver(object):
     @property
     def qpu(self):
         "Is this a QPU-based solver?"
-        return self.properties.get('category', '').lower() == 'qpu'
+        category = self.properties.get('category', '').lower()
+        if category:
+            return category == 'qpu'
+        else:
+            # fallback for legacy solvers without the `category` property
+            # TODO: remove when all production solvers are updated
+            return not (self.software or self.hybrid)
 
     @property
     def software(self):
         "Is this a software-based solver?"
-        return self.properties.get('category', '').lower() == 'software'
+        category = self.properties.get('category', '').lower()
+        if category:
+            return category == 'software'
+        else:
+            # fallback for legacy solvers without the `category` property
+            # TODO: remove when all production solvers are updated
+            return self.id.startswith('c4-sw_')
 
     @property
     def hybrid(self):
         "Is this a hybrid quantum-classical solver?"
-        return self.properties.get('category', '').lower() == 'hybrid'
+        category = self.properties.get('category', '').lower()
+        if category:
+            return category == 'hybrid'
+        else:
+            # fallback for legacy solvers without the `category` property
+            # TODO: remove when all production solvers are updated
+            return self.id.startswith('hybrid')
 
     @property
     def is_qpu(self):

diff --git a/tests/test_client.py b/tests/test_client.py
index a4f5fc9..12f1726 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -23,6 +23,7 @@ import time
 import warnings
 import unittest
 from unittest import mock
+from contextlib import contextmanager
 
 import requests.exceptions
 from plucky import merge
@@ -479,7 +480,7 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(online=True), self.solvers)
         self.assertSolvers(self.client.get_solvers(online=False), [])
 
-    def test_qpu_hybrid_software(self):
+    def test_derived_category_properties(self):
         self.assertSolvers(self.client.get_solvers(qpu=True), self.qpu_solvers)
         self.assertSolvers(self.client.get_solvers(qpu=False), self.software_solvers + self.hybrid_solvers)
         self.assertSolvers(self.client.get_solvers(software=True), self.software_solvers)
@@ -487,6 +488,30 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(hybrid=True), self.hybrid_solvers)
         self.assertSolvers(self.client.get_solvers(hybrid=False), self.qpu_solvers + self.software_solvers)
 
+    # Test fallback for legacy solvers without the `category` property
+    # TODO: remove when all production solvers are updated
+    def test_derived_category_properties_without_category(self):
+        "Category-based filtering works without explicit `category` property."
+
+        @contextmanager
+        def multi_solver_properties_patch(solvers, update):
+            """Update properties for all `solvers` at once."""
+            patchers = [mock.patch.dict(s.properties, update) for s in solvers]
+            try:
+                yield (p.start() for p in patchers)
+            finally:
+                return (p.stop() for p in patchers)
+
+        with mock.patch.object(self.software, 'id', 'c4-sw_solver3'):
+            # patch categories and re-run the category-based filtering test
+            with multi_solver_properties_patch(self.solvers, {'category': ''}):
+                self.test_derived_category_properties()
+
+            # verify patching
+            with multi_solver_properties_patch(self.solvers, {'category': 'x'}):
+                with self.assertRaises(AssertionError):
+                    self.test_derived_category_properties()
+
     def test_category(self):
         self.assertSolvers(self.client.get_solvers(category='qpu'), self.qpu_solvers)
         self.assertSolvers(self.client.get_solvers(category='software'), self.software_solvers)
diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py
index 492226c..3fdc8ec 100644
--- a/tests/test_mock_solver_loading.py
+++ b/tests/test_mock_solver_loading.py
@@ -25,6 +25,7 @@ import requests_mock
 from dwave.cloud.client import Client, Solver
 from dwave.cloud.qpu import Client as QPUClient
 from dwave.cloud.sw import Client as SoftwareClient
+from dwave.cloud.hybrid import Client as HybridClient
 from dwave.cloud.exceptions import (
     SolverPropertyMissingError, ConfigFileReadError, ConfigFileParseError,
     SolverError, SolverNotFoundError)
@@ -226,6 +227,12 @@ class MockSolverLoading(unittest.TestCase):
         self.assertFalse(SoftwareClient.is_solver_handled(solver_object('test', 'hybrid')))
         self.assertFalse(SoftwareClient.is_solver_handled(solver_object('test', 'whatever')))
         self.assertFalse(SoftwareClient.is_solver_handled(None))
+        # hybrid client
+        self.assertFalse(HybridClient.is_solver_handled(solver_object('test', 'qpu')))
+        self.assertFalse(HybridClient.is_solver_handled(solver_object('test', 'software')))
+        self.assertTrue(HybridClient.is_solver_handled(solver_object('test', 'hybrid')))
+        self.assertFalse(HybridClient.is_solver_handled(solver_object('test', 'whatever')))
+        self.assertFalse(HybridClient.is_solver_handled(None))
 
     def test_solver_feature_properties(self):
         self.assertTrue(solver_object('solver', 'qpu').qpu)
@@ -275,6 +282,26 @@ class MockSolverLoading(unittest.TestCase):
         del data['status']
         self.assertTrue(Solver(None, data).online)
 
+    # Test fallback for legacy solvers without the `category` property
+    # TODO: remove when all production solvers are updated
+    def test_solver_with_category_missing(self):
+
+        # client type filtering support
+        self.assertTrue(QPUClient.is_solver_handled(solver_object('solver', cat='')))
+        self.assertTrue(SoftwareClient.is_solver_handled(solver_object('c4-sw_x', cat='')))
+        self.assertTrue(HybridClient.is_solver_handled(solver_object('hybrid_x', cat='')))
+
+        # derived properties are correct
+        self.assertTrue(solver_object('solver', cat='').qpu)
+        self.assertFalse(solver_object('solver', cat='').software)
+        self.assertFalse(solver_object('solver', cat='').hybrid)
+        self.assertFalse(solver_object('c4-sw_x', cat='').qpu)
+        self.assertTrue(solver_object('c4-sw_x', cat='').software)
+        self.assertFalse(solver_object('c4-sw_x', cat='').hybrid)
+        self.assertFalse(solver_object('hybrid_x', cat='').qpu)
+        self.assertFalse(solver_object('hybrid_x', cat='').software)
+        self.assertTrue(solver_object('hybrid_x', cat='').hybrid)
+
 
 class RequestEvent(Exception):
     """Throws exception when mocked client submits an HTTP request."""

Add support for solvers without `category` property
In [`0.7.3`](https://github.com/dwavesystems/dwave-cloud-client/releases/tag/0.7.3)+ we use solver's `category` property to filter by type.

Since some old solvers are missing this property, we'll have to partially revert a nice clean-up done in #379 in order to fallback to the old hacky method of determining solver type (category) if the `category` property is missing.
tests/test_mock_solver_loading.py::MockSolverLoading::test_solver_with_category_missing
tests/test_client.py::ClientFactory::test_client_cert_from_config tests/test_client.py::ClientFactory::test_client_cert_from_kwargs tests/test_client.py::ClientFactory::test_client_type tests/test_client.py::ClientFactory::test_custom_kwargs tests/test_client.py::ClientFactory::test_custom_kwargs_overrides_config tests/test_client.py::ClientFactory::test_default tests/test_client.py::ClientFactory::test_headers_from_config tests/test_client.py::ClientFactory::test_headers_from_kwargs tests/test_client.py::ClientFactory::test_legacy_config_load_fallback tests/test_client.py::ClientFactory::test_solver_features_from_config tests/test_client.py::ClientFactory::test_solver_features_kwargs_override_config tests/test_client.py::ClientFactory::test_solver_name_from_config tests/test_client.py::ClientFactory::test_solver_name_overrides_config_features tests/test_client.py::FeatureBasedSolverSelection::test_anneal_schedule tests/test_client.py::FeatureBasedSolverSelection::test_availability_combo tests/test_client.py::FeatureBasedSolverSelection::test_category tests/test_client.py::FeatureBasedSolverSelection::test_default tests/test_client.py::FeatureBasedSolverSelection::test_derived_category_properties tests/test_client.py::FeatureBasedSolverSelection::test_derived_category_properties_without_category tests/test_client.py::FeatureBasedSolverSelection::test_lower_noise_derived_property tests/test_client.py::FeatureBasedSolverSelection::test_membership_ops tests/test_client.py::FeatureBasedSolverSelection::test_name tests/test_client.py::FeatureBasedSolverSelection::test_nested_properties_intermediate_key_lookup tests/test_client.py::FeatureBasedSolverSelection::test_nested_properties_leaf_lookup tests/test_client.py::FeatureBasedSolverSelection::test_num_qubits tests/test_client.py::FeatureBasedSolverSelection::test_online tests/test_client.py::FeatureBasedSolverSelection::test_order_by_callable tests/test_client.py::FeatureBasedSolverSelection::test_order_by_edgecases tests/test_client.py::FeatureBasedSolverSelection::test_order_by_string tests/test_client.py::FeatureBasedSolverSelection::test_parameter_availability_check tests/test_client.py::FeatureBasedSolverSelection::test_property_availability_check tests/test_client.py::FeatureBasedSolverSelection::test_range_boolean_combo tests/test_client.py::FeatureBasedSolverSelection::test_range_ops tests/test_client.py::FeatureBasedSolverSelection::test_regex tests/test_client.py::FeatureBasedSolverSelection::test_relational_ops tests/test_client.py::FeatureBasedSolverSelection::test_set_ops tests/test_client.py::FeatureBasedSolverSelection::test_solvers_deprecation tests/test_mock_solver_loading.py::MockConnectivityTests::test_bad_token tests/test_mock_solver_loading.py::MockConnectivityTests::test_bad_url tests/test_mock_solver_loading.py::MockConnectivityTests::test_good_connection tests/test_mock_solver_loading.py::MockSolverLoading::test_load_all_solvers tests/test_mock_solver_loading.py::MockSolverLoading::test_load_missing_solver tests/test_mock_solver_loading.py::MockSolverLoading::test_load_solver tests/test_mock_solver_loading.py::MockSolverLoading::test_load_solver_broken_response tests/test_mock_solver_loading.py::MockSolverLoading::test_load_solver_missing_data tests/test_mock_solver_loading.py::MockSolverLoading::test_solver_feature_properties tests/test_mock_solver_loading.py::MockSolverLoading::test_solver_filtering_in_client tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_env_args_set tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_env_with_file_set tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_explicit_only tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_explicit_with_file tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_file_read_error tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_nonexisting_file tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_only_file tests/test_mock_solver_loading.py::MockLegacyConfiguration::test_only_file_key tests/test_mock_solver_loading.py::MockConfiguration::test_custom_options