diff --git a/pyls/_utils.py b/pyls/_utils.py
new file mode 100644
index 0000000..b185454
--- /dev/null
+++ b/pyls/_utils.py
@@ -0,0 +1,16 @@
+# Copyright 2017 Palantir Technologies, Inc.
+import functools
+import threading
+
+
+def debounce(interval_s):
+    """Debounce calls to this function until interval_s seconds have passed."""
+    def wrapper(func):
+        @functools.wraps(func)
+        def debounced(*args, **kwargs):
+            if hasattr(debounced, '_timer'):
+                debounced._timer.cancel()
+            debounced._timer = threading.Timer(interval_s, func, args, kwargs)
+            debounced._timer.start()
+        return debounced
+    return wrapper
diff --git a/pyls/python_ls.py b/pyls/python_ls.py
index 2f27bc3..2d8f3f4 100644
--- a/pyls/python_ls.py
+++ b/pyls/python_ls.py
@@ -1,10 +1,13 @@
+# Copyright 2017 Palantir Technologies, Inc.
 import logging
-from . import config, lsp, plugins
+from . import config, lsp, plugins, _utils
 from .language_server import LanguageServer
 from .workspace import Workspace
 
 log = logging.getLogger(__name__)
 
+LINT_DEBOUNCE_S = 0.5  # 500 ms
+
 
 class PythonLanguageServer(LanguageServer):
 
@@ -87,6 +90,7 @@ class PythonLanguageServer(LanguageServer):
     def hover(self, doc_uri, position):
         return self._hook(self._hooks.pyls_hover, doc_uri, position=position) or {'contents': ''}
 
+    @_utils.debounce(LINT_DEBOUNCE_S)
     def lint(self, doc_uri):
         self.workspace.publish_diagnostics(doc_uri, flatten(self._hook(
             self._hooks.pyls_lint, doc_uri

diff --git a/test/test_language_server.py b/test/test_language_server.py
index 7f0de1b..408fccc 100644
--- a/test/test_language_server.py
+++ b/test/test_language_server.py
@@ -85,7 +85,7 @@ def test_linting(client_server):
     assert 'capabilities' in response['result']
 
     # didOpen
-    client.call('textDocument/didOpen', {
+    client.notify('textDocument/didOpen', {
         'textDocument': {'uri': 'file:///test', 'text': 'import sys'}
     })
     response = _get_notification(client)
diff --git a/test/test_utils.py b/test/test_utils.py
new file mode 100644
index 0000000..911519c
--- /dev/null
+++ b/test/test_utils.py
@@ -0,0 +1,26 @@
+# Copyright 2017 Palantir Technologies, Inc.
+import time
+from pyls import _utils
+
+
+def test_debounce():
+    interval = 0.1
+
+    @_utils.debounce(0.1)
+    def call_m():
+        call_m._count += 1
+
+    call_m._count = 0
+
+    call_m()
+    call_m()
+    call_m()
+    assert call_m._count == 0
+
+    time.sleep(interval * 2)
+    call_m()
+    assert call_m._count == 1
+
+    time.sleep(interval * 2)
+    call_m()
+    assert call_m._count == 2

`lint` called too frequently resulting in spurious warnings
The really annoying one is `W291` trailing whitespace. I wouldn't want to disable this warning because it's actually useful (although you can highlight trailing whitespace anyway), but since the `lint` command runs on every change, then every time you enter a space, the warning appears. In `nvim` this means my signcolumn / gutter is constantly appearing and re-appearing, which makes the text area shift left and right a column.

I think the problem could be remedied with more logic [here](https://github.com/palantir/python-language-server/blob/develop/pyls/python_ls.py#L112#L119) instead of calling `self.lint` on every change. Maybe every line would be a better frequency?

I would be happy to implement this at some point.
test/test_language_server.py::test_initialize test/test_language_server.py::test_file_closed test/test_language_server.py::test_missing_message test/test_language_server.py::test_linting test/test_utils.py::test_debounce