diff --git a/os_collect_config/collect.py b/os_collect_config/collect.py
index 084b41c..dfbafe1 100644
--- a/os_collect_config/collect.py
+++ b/os_collect_config/collect.py
@@ -31,10 +31,11 @@ from os_collect_config import heat_local
 from os_collect_config import keystone
 from os_collect_config import local
 from os_collect_config.openstack.common import log
+from os_collect_config import request
 from os_collect_config import version
 from oslo.config import cfg
 
-DEFAULT_COLLECTORS = ['heat_local', 'ec2', 'cfn', 'heat']
+DEFAULT_COLLECTORS = ['heat_local', 'ec2', 'cfn', 'heat', 'request']
 opts = [
     cfg.StrOpt('command', short='c',
                help='Command to run on metadata changes. If specified,'
@@ -83,7 +84,8 @@ COLLECTORS = {ec2.name: ec2,
               cfn.name: cfn,
               heat.name: heat,
               heat_local.name: heat_local,
-              local.name: local}
+              local.name: local,
+              request.name: request}
 
 
 def setup_conf():
@@ -102,6 +104,9 @@ def setup_conf():
     heat_group = cfg.OptGroup(name='heat',
                               title='Heat Metadata options')
 
+    request_group = cfg.OptGroup(name='request',
+                                 title='Request Metadata options')
+
     keystone_group = cfg.OptGroup(name='keystone',
                                   title='Keystone auth options')
 
@@ -110,12 +115,14 @@ def setup_conf():
     CONF.register_group(heat_local_group)
     CONF.register_group(local_group)
     CONF.register_group(heat_group)
+    CONF.register_group(request_group)
     CONF.register_group(keystone_group)
     CONF.register_cli_opts(ec2.opts, group='ec2')
     CONF.register_cli_opts(cfn.opts, group='cfn')
     CONF.register_cli_opts(heat_local.opts, group='heat_local')
     CONF.register_cli_opts(local.opts, group='local')
     CONF.register_cli_opts(heat.opts, group='heat')
+    CONF.register_cli_opts(request.opts, group='request')
     CONF.register_cli_opts(keystone.opts, group='keystone')
 
     CONF.register_cli_opts(opts)
diff --git a/os_collect_config/exc.py b/os_collect_config/exc.py
index 74d2d22..78c9f19 100644
--- a/os_collect_config/exc.py
+++ b/os_collect_config/exc.py
@@ -50,5 +50,13 @@ class LocalMetadataNotAvailable(SourceNotAvailable):
     """The local metadata is not available."""
 
 
+class RequestMetadataNotAvailable(SourceNotAvailable):
+    """The request metadata is not available."""
+
+
+class RequestMetadataNotConfigured(SourceNotAvailable):
+    """The request metadata is not fully configured."""
+
+
 class InvalidArguments(ValueError):
     """Invalid arguments."""
diff --git a/os_collect_config/request.py b/os_collect_config/request.py
new file mode 100644
index 0000000..5910ee4
--- /dev/null
+++ b/os_collect_config/request.py
@@ -0,0 +1,93 @@
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import calendar
+import json
+import time
+
+from oslo.config import cfg
+
+from os_collect_config import common
+from os_collect_config import exc
+from os_collect_config.openstack.common import log
+
+CONF = cfg.CONF
+logger = log.getLogger(__name__)
+
+opts = [
+    cfg.StrOpt('metadata-url',
+               help='URL to query for metadata'),
+]
+name = 'request'
+
+
+class Collector(object):
+    def __init__(self, requests_impl=common.requests):
+        self._requests_impl = requests_impl
+        self._session = requests_impl.Session()
+        self.last_modified = None
+
+    def check_fetch_content(self, headers):
+        '''Raises RequestMetadataNotAvailable if metadata should not be
+        fetched.
+        '''
+
+        # no last-modified header, so fetch
+        lm = headers.get('last-modified')
+        if not lm:
+            return
+
+        last_modified = calendar.timegm(
+            time.strptime(lm, '%a, %d %b %Y %H:%M:%S %Z'))
+
+        # first run, so fetch
+        if not self.last_modified:
+            return last_modified
+
+        if last_modified < self.last_modified:
+            logger.warn(
+                'Last-Modified is older than previous collection')
+
+        if last_modified <= self.last_modified:
+            raise exc.RequestMetadataNotAvailable
+        return last_modified
+
+    def collect(self):
+        if CONF.request.metadata_url is None:
+            logger.warn('No metadata_url configured.')
+            raise exc.RequestMetadataNotConfigured
+        url = CONF.request.metadata_url
+        final_content = {}
+
+        try:
+            head = self._session.head(url)
+            last_modified = self.check_fetch_content(head.headers)
+
+            content = self._session.get(url)
+            content.raise_for_status()
+            self.last_modified = last_modified
+
+        except self._requests_impl.exceptions.RequestException as e:
+            logger.warn(e)
+            raise exc.RequestMetadataNotAvailable
+        try:
+            value = json.loads(content.text)
+        except ValueError as e:
+            logger.warn(
+                'Failed to parse as json. (%s)' % e)
+            raise exc.RequestMetadataNotAvailable
+        final_content.update(value)
+
+        return [('request', final_content)]
diff --git a/os_collect_config/tests/test_collect.py b/os_collect_config/tests/test_collect.py
index 09dbc43..1feab93 100644
--- a/os_collect_config/tests/test_collect.py
+++ b/os_collect_config/tests/test_collect.py
@@ -35,6 +35,7 @@ from os_collect_config.tests import test_cfn
 from os_collect_config.tests import test_ec2
 from os_collect_config.tests import test_heat
 from os_collect_config.tests import test_heat_local
+from os_collect_config.tests import test_request
 
 
 def _setup_local_metadata(test_case):
@@ -63,7 +64,8 @@ class TestCollect(testtools.TestCase):
             'heat': {
                 'keystoneclient': test_heat.FakeKeystoneClient(self),
                 'heatclient': test_heat.FakeHeatClient(self)
-            }
+            },
+            'request': {'requests_impl': test_request.FakeRequests},
         }
         return collect.__main__(args=fake_args,
                                 collector_kwargs_map=collector_kwargs_map)
@@ -338,6 +340,7 @@ class TestCollectAll(testtools.TestCase):
         cfg.CONF.heat.project_id = '9f6b09df-4d7f-4a33-8ec3-9924d8f46f10'
         cfg.CONF.heat.stack_id = 'a/c482680f-7238-403d-8f76-36acf0c8e0aa'
         cfg.CONF.heat.resource_name = 'server'
+        cfg.CONF.request.metadata_url = 'http://127.0.0.1:8000/my_metadata/'
 
     @mock.patch.object(ks_discover.Discover, '__init__')
     @mock.patch.object(ks_discover.Discover, 'url_for')
@@ -352,7 +355,8 @@ class TestCollectAll(testtools.TestCase):
                 'heat': {
                     'keystoneclient': test_heat.FakeKeystoneClient(self),
                     'heatclient': test_heat.FakeHeatClient(self)
-                }
+                },
+                'request': {'requests_impl': test_request.FakeRequests},
             }
         if collectors is None:
             collectors = cfg.CONF.collectors
@@ -366,7 +370,8 @@ class TestCollectAll(testtools.TestCase):
         (changed_keys, paths) = self._call_collect_all(
             store=True, collector_kwargs_map=collector_kwargs_map)
         if expected_changed is None:
-            expected_changed = set(['heat_local', 'cfn', 'ec2', 'heat'])
+            expected_changed = set(
+                ['heat_local', 'cfn', 'ec2', 'heat', 'request'])
         self.assertEqual(expected_changed, changed_keys)
         self.assertThat(paths, matchers.IsInstance(list))
         for path in paths:
@@ -384,10 +389,11 @@ class TestCollectAll(testtools.TestCase):
             'heat': {
                 'keystoneclient': test_heat.FakeKeystoneClient(self),
                 'heatclient': test_heat.FakeHeatClient(self)
-            }
+            },
+            'request': {'requests_impl': test_request.FakeRequests},
         }
         expected_changed = set((
-            'heat_local', 'ec2', 'cfn', 'heat',
+            'heat_local', 'ec2', 'cfn', 'heat', 'request',
             'dep-name1', 'dep-name2', 'dep-name3'))
         self._test_collect_all_store(collector_kwargs_map=soft_config_map,
                                      expected_changed=expected_changed)
@@ -422,7 +428,8 @@ class TestCollectAll(testtools.TestCase):
             'heat': {
                 'keystoneclient': test_heat.FakeKeystoneClient(self),
                 'heatclient': test_heat.FakeHeatClient(self)
-            }
+            },
+            'request': {'requests_impl': test_request.FakeRequests},
         }
         (changed_keys, paths) = self._call_collect_all(
             store=True, collector_kwargs_map=soft_config_map)
diff --git a/os_collect_config/tests/test_request.py b/os_collect_config/tests/test_request.py
new file mode 100644
index 0000000..a48f969
--- /dev/null
+++ b/os_collect_config/tests/test_request.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import calendar
+import json
+import time
+
+import fixtures
+from oslo.config import cfg
+import requests
+import testtools
+from testtools import matchers
+
+from os_collect_config import collect
+from os_collect_config import exc
+from os_collect_config import request
+
+
+META_DATA = {u'int1': 1,
+             u'strfoo': u'foo',
+             u'map_ab': {
+                 u'a': 'apple',
+                 u'b': 'banana',
+             }}
+
+
+class FakeResponse(dict):
+    def __init__(self, text, headers=None):
+        self.text = text
+        self.headers = headers
+
+    def raise_for_status(self):
+        pass
+
+
+class FakeRequests(object):
+    exceptions = requests.exceptions
+
+    class Session(object):
+        def get(self, url):
+            return FakeResponse(json.dumps(META_DATA))
+
+        def head(self, url):
+            return FakeResponse('', headers={
+                'last-modified': time.strftime(
+                    "%a, %d %b %Y %H:%M:%S %Z", time.gmtime())})
+
+
+class FakeFailRequests(object):
+    exceptions = requests.exceptions
+
+    class Session(object):
+        def get(self, url):
+            raise requests.exceptions.HTTPError(403, 'Forbidden')
+
+        def head(self, url):
+            raise requests.exceptions.HTTPError(403, 'Forbidden')
+
+
+class TestRequestBase(testtools.TestCase):
+    def setUp(self):
+        super(TestRequestBase, self).setUp()
+        self.log = self.useFixture(fixtures.FakeLogger())
+        collect.setup_conf()
+        cfg.CONF.request.metadata_url = 'http://127.0.0.1:8000/my_metadata'
+
+
+class TestRequest(TestRequestBase):
+
+    def test_collect_request(self):
+        req_collect = request.Collector(requests_impl=FakeRequests)
+        self.assertIsNone(req_collect.last_modified)
+        req_md = req_collect.collect()
+        self.assertIsNotNone(req_collect.last_modified)
+        self.assertThat(req_md, matchers.IsInstance(list))
+        self.assertEqual('request', req_md[0][0])
+        req_md = req_md[0][1]
+
+        for k in ('int1', 'strfoo', 'map_ab'):
+            self.assertIn(k, req_md)
+            self.assertEqual(req_md[k], META_DATA[k])
+
+        self.assertEqual('', self.log.output)
+
+    def test_collect_request_fail(self):
+        req_collect = request.Collector(requests_impl=FakeFailRequests)
+        self.assertRaises(exc.RequestMetadataNotAvailable, req_collect.collect)
+        self.assertIn('Forbidden', self.log.output)
+
+    def test_collect_request_no_metadata_url(self):
+        cfg.CONF.request.metadata_url = None
+        req_collect = request.Collector(requests_impl=FakeRequests)
+        self.assertRaises(exc.RequestMetadataNotConfigured,
+                          req_collect.collect)
+        self.assertIn('No metadata_url configured', self.log.output)
+
+    def test_check_fetch_content(self):
+        req_collect = request.Collector()
+
+        now_secs = calendar.timegm(time.gmtime())
+        now_str = time.strftime("%a, %d %b %Y %H:%M:%S %Z",
+                                time.gmtime(now_secs))
+
+        future_secs = calendar.timegm(time.gmtime()) + 10
+        future_str = time.strftime("%a, %d %b %Y %H:%M:%S %Z",
+                                   time.gmtime(future_secs))
+
+        past_secs = calendar.timegm(time.gmtime()) - 10
+        past_str = time.strftime("%a, %d %b %Y %H:%M:%S %Z",
+                                 time.gmtime(past_secs))
+
+        self.assertIsNone(req_collect.last_modified)
+
+        # first run always collects
+        self.assertEqual(
+            now_secs,
+            req_collect.check_fetch_content({'last-modified': now_str}))
+
+        # second run unmodified, does not collect
+        req_collect.last_modified = now_secs
+        self.assertRaises(exc.RequestMetadataNotAvailable,
+                          req_collect.check_fetch_content,
+                          {'last-modified': now_str})
+
+        # run with later date, collects
+        self.assertEqual(
+            future_secs,
+            req_collect.check_fetch_content({'last-modified': future_str}))
+
+        # run with earlier date, does not collect
+        self.assertRaises(exc.RequestMetadataNotAvailable,
+                          req_collect.check_fetch_content,
+                          {'last-modified': past_str})
+
+        # run no last-modified header, collects
+        self.assertIsNone(req_collect.check_fetch_content({}))