diff --git a/django/forms/models.py b/django/forms/models.py
index de5f1ab..4cd518f 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -414,6 +414,60 @@ class BaseModelFormSet(BaseFormSet):
             self.save_m2m = save_m2m
         return self.save_existing_objects(commit) + self.save_new_objects(commit)
 
+    def clean(self):
+        self.validate_unique()
+
+    def validate_unique(self):
+        from django.db.models.fields import FieldDoesNotExist, Field as ModelField
+        unique_checks = []
+        first_form = self.forms[0]
+        for name, field in first_form.fields.iteritems():
+            try:
+                f = first_form.instance._meta.get_field_by_name(name)[0]
+            except FieldDoesNotExist:
+                continue
+            if not isinstance(f, ModelField):
+                # This is an extra field that happens to have a name that matches,
+                # for example, a related object accessor for this model.  So
+                # get_field_by_name found it, but it is not a Field so do not proceed
+                # to use it as if it were.
+                continue
+            if f.unique:
+                unique_checks.append((name,))
+        unique_together = []
+        for unique_together_check in first_form.instance._meta.unique_together:
+            fields_on_form = [f for f in unique_together_check if f in first_form.fields]
+            if len(fields_on_form) == len(unique_together_check):
+                unique_together.append(unique_together_check)
+        unique_checks.extend(unique_together)
+        errors = []
+        for unique_check in unique_checks:
+            seen_data = set()
+            for form in self.forms:
+                if not hasattr(form, "cleaned_data"):
+                    continue
+                if [f for f in unique_check if f in form.cleaned_data and form.cleaned_data[f] is not None]:
+                    row_data = tuple([form.cleaned_data[field] for field in unique_check])
+                    if row_data in seen_data:
+                        errors.append(self.get_unique_error_message(unique_check))
+                        break
+                    else:
+                        seen_data.add(row_data)
+        if errors:
+            raise ValidationError(errors)
+
+    def get_unique_error_message(self, unique_check):
+        if len(unique_check) == 1:
+            return _("You have entered duplicate data for %(field)s. It "
+                "should be unique.") % {
+                    "field": unique_check[0],
+                }
+        else:
+            return _("You have entered duplicate data for %(field)s. They "
+                "should be unique together.") % {
+                    "field": get_text_list(unique_check, _("and")),
+                }
+
     def save_existing_objects(self, commit=True):
         self.changed_objects = []
         self.deleted_objects = []
@@ -564,6 +618,10 @@ class BaseInlineFormSet(BaseModelFormSet):
                 label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name))
             )
 
+    def get_unique_error_message(self, unique_check):
+        unique_check = [field for field in unique_check if field != self.fk.name]
+        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check)
+
 def _get_foreign_key(parent_model, model, fk_name=None):
     """
     Finds and returns the ForeignKey from model to parent if there is one.
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index be67a38..81d4642 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -515,6 +515,22 @@ exclude::
 
 .. _saving-objects-in-the-formset:
 
+Overriding clean() method
+-------------------------
+
+You can override the ``clean()`` method to provide custom validation to
+the whole formset at once. By default, the ``clean()`` method will validate
+that none of the data in the formsets violate the unique constraints on your
+model (both field ``unique`` and model ``unique_together``). To maintain this
+default behavior be sure you call the parent's ``clean()`` method::
+
+    class MyModelFormSet(BaseModelFormSet):
+        def clean(self):
+            super(MyModelFormSet, self).clean()
+            # example custom validation across forms in the formset:
+            for form in self.forms:
+                # your custom formset validation
+
 Saving objects in the formset
 -----------------------------
 
@@ -599,6 +615,17 @@ than that of a "normal" formset. The only difference is that we call
 ``formset.save()`` to save the data into the database. (This was described
 above, in :ref:`saving-objects-in-the-formset`.)
 
+
+Overiding ``clean()`` on a ``model_formset``
+--------------------------------------------
+
+Just like with ``ModelForms``, by default the ``clean()`` method of a 
+``model_formset`` will validate that none of the items in the formset validate 
+the unique constraints on your model(either unique or unique_together).  If you 
+want to overide the ``clean()`` method on a ``model_formset`` and maintain this 
+validation, you must call the parent classes ``clean`` method.
+
+
 Using a custom queryset
 ~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
index d8cbe34..6906e59 100644
--- a/tests/modeltests/model_formsets/models.py
+++ b/tests/modeltests/model_formsets/models.py
@@ -22,6 +22,12 @@ class BetterAuthor(Author):
 class Book(models.Model):
     author = models.ForeignKey(Author)
     title = models.CharField(max_length=100)
+    
+    class Meta:
+        unique_together = (
+            ('author', 'title'),
+        )
+        ordering = ['id']
 
     def __unicode__(self):
         return self.title
@@ -934,4 +940,66 @@ True
 >>> formset.get_queryset()
 [<Player: Bobby>]
 
+# Prevent duplicates from within the same formset
+>>> FormSet = modelformset_factory(Product, extra=2)
+>>> data = {
+...     'form-TOTAL_FORMS': 2,
+...     'form-INITIAL_FORMS': 0,
+...     'form-0-slug': 'red_car',
+...     'form-1-slug': 'red_car',
+... }
+>>> formset = FormSet(data)
+>>> formset.is_valid()
+False
+>>> formset._non_form_errors
+[u'You have entered duplicate data for slug. It should be unique.']
+
+>>> FormSet = modelformset_factory(Price, extra=2)
+>>> data = {
+...     'form-TOTAL_FORMS': 2,
+...     'form-INITIAL_FORMS': 0,
+...     'form-0-price': '25',
+...     'form-0-quantity': '7',
+...     'form-1-price': '25',
+...     'form-1-quantity': '7',
+... }
+>>> formset = FormSet(data)
+>>> formset.is_valid()
+False
+>>> formset._non_form_errors
+[u'You have entered duplicate data for price and quantity. They should be unique together.']
+
+# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled.
+# this will fail with a KeyError if broken.
+>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
+>>> data = {
+...     'form-TOTAL_FORMS': '2',
+...     'form-INITIAL_FORMS': '0',
+...     'form-0-price': '24',
+...     'form-1-price': '24',
+... }
+>>> formset = FormSet(data)
+>>> formset.is_valid()
+True
+
+>>> FormSet = inlineformset_factory(Author, Book, extra=0)
+>>> author = Author.objects.order_by('id')[0]
+>>> book_ids = author.book_set.values_list('id', flat=True)
+>>> data = {
+...     'book_set-TOTAL_FORMS': '2',
+...     'book_set-INITIAL_FORMS': '2',
+...     
+...     'book_set-0-title': 'The 2008 Election',
+...     'book_set-0-author': str(author.id),
+...     'book_set-0-id': str(book_ids[0]),
+...     
+...     'book_set-1-title': 'The 2008 Election',
+...     'book_set-1-author': str(author.id),
+...     'book_set-1-id': str(book_ids[1]),
+... }
+>>> formset = FormSet(data=data, instance=author)
+>>> formset.is_valid()
+False
+>>> formset._non_form_errors
+[u'You have entered duplicate data for title. It should be unique.']
 """}
